在 Qt Designer 內使用自定義的 widget


Qt 基本上是一個很有彈性的跨平台圖形介面框架,他本身就提供了很多 widget 可以使用。而除了可以透過程式碼來設計界面外,也可以使用官方提供的 Qt Designer(Qt 設計師、官網),用所見即所得的方法來拉出自己的介面,算是相當地方便。

如果官方提供的 widget 不夠用的話,也可以自己組合既有的元件、或是重頭撰寫一個符合自己的元件來用;這部分有需要話,可以參考官方的《Analog Clock Example》。

而如果想在 Designer 裡面使用自己定義的 widget 的話,就稍微麻煩一點了。這篇就是稍微整理一下,要怎麼在 Qt Designer 裡面、使用自己定義的 widget。


自己定義的 widget

首先,這邊拿來作範例、自己定義的 Qt Widget 叫做「QValueSlider」,是一個把 QDoubleSpinBoxQSlider 合併在一起、有連動的複合式 widget。他的介面是下圖的樣子:

程式碼的部分,可以參考 GitHub。這邊的專案是會把它建置成一個 static library,方便之後重複使用。


在 Designer 中「提升」

當完成了這樣的自定義的 Qt Widget 後,要在 Qt Designer 中使用,最簡單的方法,是直接透過 Designer 中「提升」這個功能(原文是「Promote 」、參考),來把既有的 widget 轉換成要使用的 widget。

這個方法用起來比較麻煩,基本上是:

  1. 將左邊的「Widget」(代表最基礎的 QWidget)拉到 Form上
  2. 在物件上點選右鍵,然後選擇「提升到…」
  3. 這邊會跳出下面的對話框

    在下面「新增提升的元件」中輸入類別名稱、以及標頭檔後,按下「新增」,就可以把自己定義的 widget 類別加入上方「已提升類別」。
  4. 在上方選擇要轉換的類別後,點選下方的「提升」,就可以完成了。

這時候,就可以看到剛剛拉進來的物件,類別已經變成 QValueSlider 了~

這個方法的好處,是在定義自己的 widget 的時候算是相對簡單,沒有什麼額外的工作要做。但是缺點呢…有點明顯,那就是在 Qt Designer 中,不但沒有介面的預覽(可以看到上圖選取的 widget 還是一片空白),所有的 signal 和 slot 也都沒有帶進來。

這些缺點雖然不是致命性的,整個系統還是可以用,但是沒有 widget 的預覽有的時候會讓版面調整的工作更加麻煩;而沒有帶入必要的 signal / slot,雖然也可以在 Designer 中手動加入,但還是會造成事件串連的麻煩。


額外加入給 Designer 用的 Plugin

如果想要讓自己寫的 widget 在 Designer 中可以像官方提供的 widget 一樣,有屬性資料、有預覽、有 signal / slot 的話,該怎麼做呢?

實際上,Qt Designer 本身就有提供 Plugin 的架構,可以讓開發者去把自己的 widget 封包成 Designer 用的 plugin、讓他們有類似原生 widget 一樣的便利性。

這部分主要可以參考官方的《Custom Widget Plugin Example》。

而由於這邊是要產生的是給 Qt Designer 用的 plugin、最後需要的是一個 DLL 檔,所以這邊就不能用前面的 static library、而是要用 dynamic library 才行。

Heresy 這邊的作法,是想說讓本來的 Widget 專案保持乾淨,所以就另外建立一個專案、來放 plugin 的東西了~這個專案可以參考 GitHub

如此一來,本來放 widget 的專案還是可以繼續使用,不需要修改;同時,在產生出來的程式碼的部分,也不會包含 plugin 的東西。

這邊基本上要做的事,就是針對已經寫好的 widget、另外定義一個繼承 QtUiPlugin 的 QDesignerCustomWidgetInterface 的類別、透過重新實作他的函式來回傳必要的資訊、並建立要使用的 widget。

這邊的定義就是下面的樣子:

#pragma once
 
#include <QtUiPlugin/QDesignerCustomWidgetInterface>
 
class QValueSliderPlugin : public QObject, public QDesignerCustomWidgetInterface
{
  Q_OBJECT
  Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidgetInterface")
  Q_INTERFACES(QDesignerCustomWidgetInterface)
 
public:
  explicit QValueSliderPlugin(QObject* parent = nullptr);
 
  QString name() const override;
  QString group() const override;
  QString toolTip() const override;
  QString whatsThis() const override;
  bool isContainer() const override;
  void initialize(QDesignerFormEditorInterface*) override;
  bool isInitialized() const override;
  QString includeFile() const override;
  QIcon icon() const override;
  QString domXml() const override;
  QWidget* createWidget(QWidget* parent) override;
};

這邊除了要繼承 QDesignerCustomWidgetInterface 外,也需要透過 Qt 的 Q_PLUGIN_METADATAQ_INTERFACES 這兩個巨集,來告訴 Qt 的 Plugin 系統相關的資訊。(這部分可以參考《How to Create Qt Plugins》)

而在這個 plugin 被使用的時候,他的 initialize() 這個函式會被呼叫,如果有要初始化的東西,就是寫在這(這個例子是空的);而當 isInitialized() 回傳 true 的時候,Designer 才會使用這個 plugin。

之後,則就是要實作各函式,回傳對應的訊息給 designer。

在顯示出來的資訊部分,包括了:

  • name()

    類別的名稱,這邊就是「QValueSlider」。
    他同時也會視顯示在「元件盒」中的名字,感覺應該是要可以修改的,不過 Heresy 這邊如果把它改成別的文字都會出問題…

  • group()

    在 Designer 的「元件盒」中的分類,這邊是「Display Widgets」。

  • icon()

    Widget 代表的圖示,可以增加識別度。
    這邊回傳的是一個空的 QIcon 物件,所以它顯示的是 Qt 預設的圖示。

  • toolTip()whatsThis()

    可以透過 toolTip() 回傳簡短的說明,並透過 whatsThis() 回傳更詳細的描述。

  • isContainer()

    告訴 Designer 這個 widget 是否是可以拿來放其他 widget 的容器。

再來,includeFile() 則是要告訴 Qt 要使用這個 widget 需要的 header 檔案的路徑,應該是指支援一個檔案。

doXml() 則是要回傳一個 Qt Designer 需要的 XML 資料,其內容大致上會是:

<ui language="c++">
<widget class="QValueSlider" name="valueSlider">
  <property name="geometry">
    <rect>
      <x>0</x>
      <y>0</y>
      <width>230</width>
      <height>20</height>
    </rect>
  </property>
</widget>
</ui>

其中,「valueSlider」這個「name」是把 widget 加到 form 的時候,預設的物件名稱,所以在取名的時候要以變數名稱的標準來取名。

再來比較重要的是這邊可以透過加入 geometry 這個屬性,來回報他預設的大小。

最後,則是 createWidget()。這個函式基本上就是產生出一個 QValueSlider 的物件,給 Designer 來使用了~

QWidget* QValueSliderPlugin::createWidget(QWidget* parent)
{
  return new QValueSlider(parent);
}

這樣都寫好了之後,將建置出來的檔案(這邊是「ValueSliderPlugin.dll」)複製到 Designer 的 plugin 路徑下,重新開啟 Designer,應該就可以在元件盒找到剛剛加入的 QValueSlider 了~

而當把 widget 加到 form 上的時候,也就會有預覽、而不再是空空的框框了~

同時,signal 和 slot 也會帶進來、不必自己增加了。

在 Heresy 這邊 plugin 的路徑是「Qt\5.15.2\msvc2019_64\plugins\designer」,不過根據每個人的系統配置,可能會在不同的地方。

而在 Qt Designer 中,也可以透過「說明」、「關於插件」,來確認目前讀取到的 plugin。


Widget 的補強

理論上,這樣做完就可以比較方便地在 Qt Designer 裡面使用自己的 widget 了。

不過,以往 Heresy 在定義 Qt 的 Widget 的時候,並不會真的去管 Qt 的 property 系統(官方文件),所以在 Designer 裡面,沒辦法像官方提供的 widget 一樣,可以在屬性編輯器裡面,直接修改預設的設定。

而如果希望能讓參數可以在屬性編輯器裡面調整的話,就得乖乖地去定義 Q_PROPERTY 了。

像這邊就是在 QValueSlider 加上了

Q_PROPERTY(int Decimals READ decimals WRITE setDecimals)
Q_PROPERTY(double Maximum READ maximum WRITE setMaximum)
Q_PROPERTY(double Minimum READ minimum WRITE setMinimum)
Q_PROPERTY(double Value READ value WRITE setValue NOTIFY valueChanged)

如此一來,Designer 才知道有那些屬性、要用那些函式來進行讀取、設定。

這樣一來,使用上就更方便了。


使用 Qt 提供的範例專案

實際上,在搭配 Qt Visual Studio Tools(官網)的情況下,也可以選擇「Qt Designer Custom Widget」這個範例專案來建立新的專案,做這件事。

不過由於這個範例專案基本上是會在同一個專案中、同時建立出新的 widget 和對應的 plugin,對於已經寫好自己的 widget 的人來說,算是不完全適用就是了。(也很好修改就是)。


附註:

  • 官方範例在原有的 Widget 定義中有加上 QDESIGNER_WIDGET_EXPORT,基本上對某些平台似乎是必要的(參考),不過在 Windows + Visual C++ 似乎可以不用。
    如果要加的話,應該得額外定義 QDESIGNER_EXPORT_WIDGETS 才能編譯。

  • Heresy 一開始在寫的時候,碰過 Widget 沒有重新實作 paintEvent() 會不能預覽的狀況,不過後來又沒這問題了。

對「在 Qt Designer 內使用自定義的 widget」的想法

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.