顯示具有 cpp 標籤的文章。 顯示所有文章
顯示具有 cpp 標籤的文章。 顯示所有文章

2021年6月28日 星期一

在 Qt Creator 中修改 MSVC 的執行階段程式庫

 在 Qt Creator中修改 MSVC 的執行階段程式庫

前言

  由於最近需要用 Qt 來使用函示庫,但函示庫是使用 MTd(Multi-threaded Debug) 來建置的,如果要和 Qt 使用的話函示庫勢必要修改選項後重建,所以想說有沒有直接改 Qt 的執行階段程式庫,在此做個紀錄。

 

內容

  Qt 在預設的情形下使用 MDd(Multi-threaded DLL Debug) 作為執行階段程式庫,如果需要修改可以在 .pro 裡面新增以下

QMAKE_CFLAGS_DEBUG += /MTd
QMAKE_CXXFLAGS_DEBUG += /MTd


範例是修改成 MTd(Multi-threaded Debug),當然也可以修改成 MD(Multi-threaded DLL)與 MT(Multi-threaded),只要將最後的參數改成需要得即可,改完後一定"重建"才會生效要注意。


參考資料

[ forum.qt.io ] How do I change the Runtime Library setting in my project in QtCreator?


相關文章與資料

Qt 的專案檔

2021年6月27日 星期日

從 HWND 取得 HINSTANCE

 從 HWND 取得 HINSTANCE

前言

  最近需要透過 HWND 來取得 HINSTANCE ,但不知是不是太久沒接觸 Win32 API ,遽然卡在編譯,在此把學習的過程做個紀錄。


內容

  透過搜尋應該會找到 GetWindowLong() 來取得,所以使用以下

#include <winuser.h>
//
HINSTANCE hIns = (HINSTANCE)GetWindowLong( hWnd , GWL_HINSTANCE);


但編譯後發現"GWL_HINSTANCE"遽然未定義!但透過 Visual Standio 的 F12 可以找到定義,在一番查找後才知道 GetWindowLong() 是給 32 位元系統在用的舊 API ,新的 API 是 GetWindowLongPtr() ,用起來如下

#include <winuser.h>
//
HINSTANCE hIns = (HINSTANCE)GetWindowLongPtr( hWnd , GWLP_HINSTANCE);


這裡要注意後方的 Marco 名稱是有些微的差異的,GWLP_HINSTANCE 不論是 32 位元或 64 位元都會存在,但 GWL_HINSTANCE 僅存在於 32 位元,如果用 F12 去 wunuser.h  裡可以發現 64 位元時該 Marcro 會被 undef 掉,這就是為什麼會發生未定義錯誤的原因。


參考資料

[ docs.microsoft.com ] GetWindowLongPtrW function

2020年12月22日 星期二

向量正規化的注意事項

 向量正規化的注意事項

前言

  最近的專案遽然發生了除0中斷,結果追了一下發生的地方,結果在自己寫的向量函示庫裡的向量正規化這裡發生,這裡把學習的過程做個紀錄。


內容

  計算向量正規化時,會先計算該向量長度,長度公式如下

向量長度公式

計算完長度後再將每個數值除以長度就可以完成正規化,但如果這個長度很小或根本是"0"的話就會發生問題,解決的方法是計算完長度後,如果小於"FLT_EPSILON"就放棄計算(或直接在結果填上零向量),"FLT_EPSILON"是 C++ 的標準 Macro ,可以解釋成 float 資料型態的最小精度。


  向量函示庫自己寫就是會碰到這種麻煩,不過也可以在這樣的過程中學習,這次可真是上了一課。


2020年11月17日 星期二

在特製 Widget 的使用右鍵選單

 在特製 Widget 的使用右鍵選單

前言

  在先前的 在 TreeView 使用右鍵選單 使用過右鍵選單,但 TreeView 是由外部控制,而特製 Widget 時是採用"繼承"的方法來控制,這次把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/CustomMenu,執行結果如下

範例的執行結果

在特製的 Widget 上按下右鍵會跳出右鍵選單。


  看到程式碼的部分,看到特製 Widget 的宣告,程式碼如下

#include <QWidget>

#include <QPainter>
#include <QMenu>
#include <QAction>
class MyWidget : public QWidget
{
  Q_OBJECT
public:
  explicit MyWidget(QWidget *parent = nullptr);

protected:
  void paintEvent(QPaintEvent *event) override;
  void onTreeViewCustomMenu(const QPoint &pos);
  void onTreeViewAction1Trigger();
  void onTreeViewAction2Trigger();
  void onTreeViewAction3Trigger();
signals:
  //
private:
  QMenu* m_pTreeViewCustomMenu;
};


右鍵選單在宣告的部分和之前的 TreeView 用馬差不多,只是這次是把事件直接宣告在 Widget ,而非 MainWindow 。接著看到實現的部分,程式碼如下

#include "mywidget.h"

MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
  this->setContextMenuPolicy(Qt::CustomContextMenu);
  connect( this , &QWidget::customContextMenuRequested , this , &MyWidget::onTreeViewCustomMenu );
  //Set up custom menu
  m_pTreeViewCustomMenu = new QMenu( this );
  QAction* pAct1 = new QAction( "Action 1" , this );
  connect( pAct1 , &QAction::triggered , this , &MyWidget::onTreeViewAction1Trigger );
  m_pTreeViewCustomMenu->addAction( pAct1 );
  QAction* pAct2 = new QAction( "Action 2" , this );
  connect( pAct2 , &QAction::triggered , this , &MyWidget::onTreeViewAction2Trigger );
  m_pTreeViewCustomMenu->addAction( pAct2 );
  QAction* pAct3 = new QAction( "Action 3" , this );
  connect( pAct3 , &QAction::triggered , this , &MyWidget::onTreeViewAction3Trigger );
  m_pTreeViewCustomMenu->addAction( pAct3 );
}

void MyWidget::paintEvent(QPaintEvent *event)
{
  QPainter painter( this );
  QPen pen( Qt::black );
  pen.setWidth( 4 );
  painter.setPen( pen );
  QRect rc( 4 , 4 , this->width() - 8, this->height() - 8 );
  painter.drawRect( rc );

}

void MyWidget::onTreeViewCustomMenu(const QPoint &pos)
{
  //
  m_pTreeViewCustomMenu->popup( this->mapToGlobal( pos ) );
}

void MyWidget::onTreeViewAction1Trigger()
{
  qDebug( "Action1 triggered" );
}
void MyWidget::onTreeViewAction2Trigger()
{
  qDebug( "Action2 triggered" );
}
void MyWidget::onTreeViewAction3Trigger()
{
  qDebug( "Action3 triggered" );
}


看到 MyWidget::MyWidget() ,開頭需要執行 setContextMenuPolicy() ,這很重要,接著綁定 customContextMenuRequested() 事件,最後是 Menu 的初始化的部分,這部分和之前的 TiewView 範例一樣。觸發事件的部分就和之前 TiewView 的範例一樣,就只是剪貼過來。 右鍵選單的用法不管是外部控制或繼承幾乎是走同一個流程,本來還擔心繼承的用法會有所不同,看來是我多慮了。


參考資料

[ doc.qt.io ] QWidget Class


相關文章與資料

[ GitLab ] HelloQt

在 TreeView 使用右鍵選單

2020年11月3日 星期二

用 JavaScript 裡綁定類別

用 JavaScript 裡綁定類別

前言

  在之前的 用 JavaScript 控制 Widget  裡使用了 Qt 的 Widget 在 JavaScript 中,如果要在 JavaScript 裡使用自己設計的類別有可能嗎?答案是可以的,要如何做呢?在此把學習的過程作紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/WrapClass,執行結果如下

範例的執行結果



範例的上方會有預設的程式碼,按下右下的"Run"可以執行,執行預設的程式碼看列印的偵錯訊息。


  要在 JavaScript 綁定自己的類別,該類別必須繼承 QObject ,這很重要!利用 Qt Creator 的精靈產生類別的話,如下圖

使用精靈產生繼承 QObject 的類別

在精靈的"Class name"填上類別名稱,在"Base class"選擇 QObject 後即可,範例已產生"MyClass",所以看到"MyClass"的宣告,如下

#include <QObject>

class MyClass : public QObject
{
  Q_OBJECT
  Q_PROPERTY(int dataInt READ myInt WRITE setMyInt)
  Q_PROPERTY(double dataDouble READ myDouble WRITE setMyDouble)
  Q_PROPERTY(QString dataString READ myString WRITE setMyString)
public:
  explicit MyClass(QObject *parent = nullptr);
//
  void setMyInt( int value );
  int myInt() const;
  void setMyDouble( double value );
  double myDouble() const;
  void setMyString( const QString& value );
  QString myString() const;
  Q_INVOKABLE void callMethod();
signals:
private:
  int m_MyInt;
  double m_MyDouble;
  QString m_MyString;
};


MyClass 會綁定三個 Property ,"m_MyInt"、"m_MyDouble"與"m_MyString",並綁定一個 Function 名為"callMethod",綁定 Function 到 JavaScript 的部分比較簡單,只需再開頭加"Q_INVOKABLE"即可,但要注意輸入的引數與回傳值得資料型態必須是 JavaScript 可以接受的型態,至於那些型態可以接受以後來研究,這次都先使用基本的型態。在綁定 Property 的部份就比較麻煩一些,綁定些要用到"Q_PROPERTY"來綁定,後方的內容要照一定的格式寫,就拿 m_MyInt 來說,"int"是綁定到 JavaScript 的型態,這個型態最後會被自動轉成 JavaScript 的 "Number" ,"dataInt" 是 Property 的名稱,可以自己命名,不一定要和 C++ 這邊的變數同名,"READ myInt"指的是讀取時所喚起的 Function ,在這裡會喚起 myInt() ,接著是"WRITE setMyInt"指的是寫的時候所喚起的 Function ,這裡會喚起 setMyInt() 。在讀與寫的 Function 也要注意資料型態的問題,字串的部份要用 QString 來綁定而非 std::string, MyClass 的實作部分沒什麼好說明就直接略過,接著看到 MainWindow::MainWindow() ,如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->runButton , &QPushButton::clicked , this , &MainWindow::onRunButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  MyClass* pMyClass = new MyClass();
  m_cJSEngine.globalObject().setProperty( "myClass" ,m_cJSEngine.newQObject( pMyClass ) );
  //
  ui->textEdit->setText( tr(
    "console.log( myClass.dataInt );\n"\
    "console.log( myClass.dataDouble );\n"\
    "console.log( myClass.dataString );\n"\
    "myClass.dataInt = 5678;\n"\
    "myClass.dataDouble = 1.414;\n"\
    "myClass.dataString = 'My string';\n"\
    "console.log( myClass.dataInt );\n"\
    "console.log( myClass.dataDouble );\n"\
    "console.log( myClass.dataString );\n"\
    "myClass.callMethod();\n"\
  ) );
}


程式開頭依舊初始化與綁定事件,這次就不說明了,接著直接 New 一個 MyClass ,透過  QJSEngine::newQObject() 來新增變數,就像上次在 用 JavaScript 控制 Widget  裡做的一樣,就著就是寫下預設的 JavaScript 程式碼,單純列印變數與喚起 Method ,這就不說明了。


參考資料

[ doc.qt.io ] QJSEngine Class


相關文章與資料

[ GitLab ] HelloQt

用 JavaScript 控制 Widget

2020年10月27日 星期二

用 JavaScript 控制 Widget

 用 JavaScript 控制 Widget 

前言

  在之前的 在 Qt 環境使用 JavaScript 裡基本的使用 JavaScript ,這次要用 JavaScript 來控制 Widget 

,在此把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/UseWidget,執行結果如下

範例的執行結果

上方會有預設的程式碼,按下右下的"Run"可以執行,執行完後可以看到左下的 PushButton 會改變顯示的字,按下後會列印偵錯訊息。


  看到 MainWindow::MainWindow() ,程式碼如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->runButton , &QPushButton::clicked , this , &MainWindow::onRunButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  m_cJSEngine.globalObject().setProperty( "myButton" , m_cJSEngine.newQObject( ui->myButton ) );
  //Default code
  ui->textEdit->setText( tr(
    "myButton.text = 'Hello myButton.';\n"\
    "myButton.clicked.connect( function( clicked ){\n"\
    "  console.log('myButton clicked!');\n"\
    "});"
  ) );
}


程式開頭綁定"Run"的事件與初始化 QJSEngine ,接著就是在 JavaScript cript 產生對應的變數,變數名為"myButton",要產生這樣的變數要透過 QJSEngine::newQObject() 來產生,參數的型態是 QObject ,由於大部份的 Widget 都繼承 QObject ,所以直接輸入就可以,不需要轉型,變數會透過 QJSEngine::globalObject() 來取得 JavaScript 的 root object ,接著透過 QJSValue::setProperty() 來新增變數就完成新增 Widget 的變數。最後的部分是新增 JavaScript 的預設程式碼 ,程式碼直接透過"myButton"來控制,範例透過"text"來控制按鈕顯示的字串,接著是事件的綁定,跟 C++ 不太一樣,要透過 (事件名稱).connect() 來綁定事件,這裡單純就顯示偵錯訊息。


  這次的範例只是簡單的應用,重點放在如何產生變數讓 JavaScript 來控制,至於要如何控制的部分目前沒找到可以查找可以用的變數(像是"text"),似乎只能用猜的(跟在設計介面的參數同名),以後有機會再多多研究。


參考資料

[ doc.qt.io ] QJSEngine Class


相關文章與資料

[ GitLab ] HelloQt

在 Qt 環境使用 JavaScript

2020年10月13日 星期二

處理 TreeView 的 Drag 與 Drop

處理 TreeView 的 Drag 與 Drop

前言

  在先前的 TreeView 的基本應用 與 在 TreeView 使用右鍵選單 裡介紹了 TreeView 的常用功能,但少提到了一個常用的功能,就是 Drag 與 Drop ,節點的資料能夠直接在 Widget 上編輯是一個非常方便的功能,在此把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/TreeView/DragAndDrop,執行結果如下

範例的執行結果


範例會給予預設的節點,並可以在 Widget 上透過拖曳(Drag)來編輯節點。這次雖然只是處理 Drag 與 Drop 的事件,但不幸的是 Qt 並沒有提供透過 Connect 事件的方式來處理,這次使用的方式是透過"繼承"的方式來處理 Drag 與 Drop 的事件,這種透過繼承的方式可以參考 特製 Widget ,兩者的差別是該篇繼承的是 QWidget ,而本篇繼承的是 QTreeView,在新增 Class 的精靈介面可以參考下圖

新增繼承 QTreeView 的類別


精靈介面加完後的宣告程式碼並不完整,像是 include 的相關程式碼並不會自動被加入,可以比對範例的宣告來看差異是什麼,接著就直接看到宣告的部分,程式碼如下

#include <QTreeView>
#include <QVector>
#include <QDragEnterEvent>
#include <QDragMoveEvent>
#include <QDragLeaveEvent>
#include <QDropEvent>
#include <QStandardItemModel>
class MyTreeView : public QTreeView
{
  Q_OBJECT
public:
  explicit MyTreeView(QWidget *parent = nullptr);
  //
  void CustomSetModel(QStandardItemModel* pModel);
  void HideColumnByList();
protected:
  void dragEnterEvent(QDragEnterEvent *event) override;
  void dragMoveEvent(QDragMoveEvent *event) override;
  void dragLeaveEvent(QDragLeaveEvent *event) override;
  void dropEvent(QDropEvent *event) override;
//
private:
  QVector<int> m_cHideColumnList;
};

處理 Drag 與 Drop 的事件有 dragEngterEvent() 、 dragMoveEvent() 、 dragLeaveEvent() 與 dropEvent() , CustomSetModel() 與 HideColumnByList() 是自己定義的 Function ,待會兒在實作的部分會進一步說明它們的功能,接下來就看到實作的部分,先看到自訂的 Function ,如下

void MyTreeView::CustomSetModel(QStandardItemModel *pModel)
{
  this->setModel( pModel );
  HideColumnByList();
}

void MyTreeView::HideColumnByList()
{
  for( auto i = 0 ; i < m_cHideColumnList.size() ; i++ )
  {
    this->hideColumn( m_cHideColumnList[i] );
  }
}


HideColumnByList() 的部分會依據成員"m_cHideColumn"裡所存的要隱藏的 Column 來喚起 QTreeView::hideColumn() ,為什麼要提供這個 Function 呢?因為 QTreeView 在處理某些事件後會把隱藏的 Column 再次變成顯示 ,所以需要這個 Function 來恢復我們期望隱藏的 Column ,CustomSetModel() 會在喚起 QTreeView::setModel() 後喚起 HideColumnByList() 隱藏指定的 Column ,接著看到主程式的 MainWindow::MainWindow() ,如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  //
  QStandardItemModel* pModel = new QStandardItemModel( ui->treeView );
  pModel->setHorizontalHeaderLabels( QStringList()<<QStringLiteral( "Name" )<<QStringLiteral( "Comment" ) );
  QStandardItem* pItemRoot1 = new QStandardItem( QStringLiteral( "root1" ) );
  //first
  pItemRoot1->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
  pItemRoot1->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
  //second
  pItemRoot1->setChild( 1 , 0 , new QStandardItem( QStringLiteral( "child1" ) ) );
  //
  pModel->appendRow( pItemRoot1 );

  //
  QStandardItem* pItemRoot2 = new QStandardItem( QStringLiteral( "root2" ) );
  pItemRoot2->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
  pItemRoot2->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
  //
  pModel->appendRow( pItemRoot2 );

  //
  ui->treeView->CustomSetModel( pModel );
}


前面就單純的加入預設節點不再贅述,可以參考 TreeView 的基本應用 ,重點在最後一行使用的是 MyTreeView::CustomSetModel() 而非 QTreeView::setModel() ,這是因為這樣可以隱藏指定的 Column 。接著看到 MyTreeView::MyTreeView() ,如下

MyTreeView::MyTreeView(QWidget *parent):
  QTreeView(parent)
{
  this->setDragEnabled( true );
  this->setDragDropMode( QAbstractItemView::InternalMove );
  //
  this->setHeaderHidden( true );
  this->setSelectionMode( QAbstractItemView::ExtendedSelection );
  //
  m_cHideColumnList.push_back( 1 );
}


由於 QTreeView 預設是把 Drag 與 Drop 的功能關閉,所以透過 setDragEnabled() 與 setDragDropMode() 來開啟,接著透過 setHeaderHidden() 來關閉上方的欄位,然後透過 setSelectionMode() 來開啟可多選,最後在成員"m_cHideColumnList"加入要隱藏的 Column ,最後就直接看到 Drag 和 Drop 事件的實作,如下

void MyTreeView::dragEnterEvent(QDragEnterEvent *event)
{
  qDebug( "dragEnterEvent" );
  //
  QTreeView::dragEnterEvent( event );
}

void MyTreeView::dragMoveEvent(QDragMoveEvent *event)
{
  qDebug( "dragMoveEvent" );
  //
  QTreeView::dragMoveEvent( event );
}

void MyTreeView::dragLeaveEvent(QDragLeaveEvent *event)
{
  qDebug( "dragLeaveEvent" );
  //
  QTreeView::dragLeaveEvent( event );
}
void MyTreeView::dropEvent(QDropEvent *event)
{
  qDebug( "dropEvent" );
  QModelIndex index = indexAt(event->pos() );
  qDebug( "dropEvent node:%s" , index.data().toString().toLocal8Bit().data() );

  DropIndicatorPosition dropIndicator = dropIndicatorPosition();
  switch ( dropIndicator ) {
  case DropIndicatorPosition::OnItem:
    qDebug("onItem");
    break;
  case DropIndicatorPosition::AboveItem:
    qDebug("AboveItem");
    break;
  case DropIndicatorPosition::BelowItem:
    qDebug("BelowItem");
    break;
  defadefault:
    qDebug("Unknown DropIndicatorPosition!");
    break;
  }
  //
  for( auto i = 0 ; i < this->selectedIndexes().size() ; i++ )
  {
    if( this->selectedIndexes().at(i).column() != 0 )
      continue;
    //
    qDebug( "source node:%s" , this->selectedIndexes().at(i).data().toString().toLocal8Bit().data() );
  }
  //
  HideColumnByList();
  //
  QTreeView::dropEvent(event);
}


 dragEngterEvent() 、 dragMoveEvent() 與 dragLeaveEvent() 由於這次用不到,所以只列印偵錯訊息,這樣方便了解事件觸發的時機,所以範例重點會放在 dropEvent() ,這裡說明一下,範例希望得到的資訊是把那些節點丟到某個節點 的相關資訊,但不幸的是 QDropEvent 裡所提供的資訊並不完整,必須搭配 QTreeView 的相關 Funciton 才能達到目的,要得到被丟到的節點資訊 可以透過 QTreeEvent::Pos() 與 QTreeView::indexAt() 來的到資料,但丟到某個節點的想法有點瑕疵,如果丟到 root 那這個節點會是無效的嗎?答案是不會,因為 Qt 的這裡設計得不是很直覺, QTreeView 提供 DropIndicatorPoisition 來說明"丟到這個節點的裡面"、"丟到這個節點的前面"與"丟到這個節點的後面",來解說要丟到哪個節點的詳細狀況,接著來接解決如何取得要丟的節點,這個資訊會存在 QTreeView::selectedIndexs() ,其實就是最後選擇的節點,因為之前有開啟多選,所以這裡可能會有多個節點,最後透過 HideColumnByList() 來隱藏指定的 Column ,最後的 QTreeView::dropEvent() 是執行 DropEvent 的預設行為,但這個預設行為並不包含把節點的資料變成丟完後的資料,整個 dropEvent() 會發生在資料被修改完之前,所以要注意這個事件是發生在丟完資料之前,也就是 QTreeView 的樹節點資料都還沒改變時。


參考資料

[ doc.qt.io ] QTreeView Class


相關文章與資料

[ GitLab ] HelloQt

特製 Widget

TreeView 的基本應用

在 TreeView 使用右鍵選單

2020年10月6日 星期二

在 Qt 方便顯示偵錯訊息的作法

 在 Qt 方便顯示偵錯訊息的作法 

前言

  在 [ GitLab ] HelloQt 的範例裡常常會用 qDebug() 來顯示,用起來類似 printf() ,但由於 Qt 有一些類別是自己定義的,像是 QString ,如果使用 qDebug() 來顯示會讓程式看起來很長,這次找到有變短的方法,在此做個紀錄。


內容

  在 [ GitLab ] HelloQt 的範例要顯示 QString 的內容在偵錯訊息會使用如下

QString str( "I'm a string" );
qDebug( "%s" , str.toLocal8Bit().data() );


可以看到要顯示 QString 需要取得字串指標時需要打很長的字,是否有比較簡單的方法呢?答案是肯定的,可以用以下

#include <QDebug>
//
QString str( "I'm a string" );
qDebug() << str;


這個方法需要 include <QDebug> ,接著就可以像 std::cout 一樣來顯示訊息,用起來相當方便。

 

參考資料

[ doc.qt.io ] QDebug Class


相關文章與資料

[ GitLab ] HelloQt

2020年9月29日 星期二

特製 Widget 的滑鼠事件

特製 Widget 的滑鼠事件

前言

  在先前的 特製 Widget 裡特製了 Widget ,這次來學習如何在特製的 Widget 裡處理滑鼠事件,在此做個紀錄。

內容

    先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/MouseEvent,ouseEvent,執行結果如下
範例的執行結果

在 Widget 上做出點擊可以看到偵錯訊息。

  由於這次的範例是依據 特製 Widget 的範例改造而來,所以不會說明如何特製一個 Widget 。特製的 Widget 是如何處理滑鼠事件呢?透過 Qt 常見的綁定事件嗎?還記得特製 Widget 時事透過繼承 QWidget 這個類別嗎?這個類別提供滑鼠事件的 Virtual function 介面,只需要做覆寫(override)的動作就可以處理滑鼠的相關事件。接著看到宣告的部分,程式碼如下
#include <QWidget>

#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
class MyWidget : public QWidget
{
  Q_OBJECT
public:
  explicit MyWidget(QWidget *parent = nullptr);
protected:
  void paintEvent(QPaintEvent *event) override;
  void mouseDoubleClickEvent(QMouseEvent *event) override;
  void mouseMoveEvent(QMouseEvent *event) override;
  void mousePressEvent(QMouseEvent *event) override;
  void mouseReleaseEvent(QMouseEvent *event) override;
  void wheelEvent(QWheelEvent *event) override;
signals:

};

滑鼠的事件介面分別為 mouseDoubleClickEvent() 、 mouseMoveEvent() 、MousePressEvent() 、mouseReleaseEvent() 與 WheelEvent() ,  mouseDoubleClickEvent() 處理雙擊事件,但要注意的是發生雙擊之前必定會發生單擊事件, mouseMoveEvent() 處理的是壓住某個按鍵後拖動的事件,MousePressEvent() 處理單擊事件, mouseReleaseEvent() 處理按鍵放開, WheelEvent() 處理耍的滾輪,由於事件處理需要用到 QMouseEvent 與 QWheelEvent ,所以記得要 include 相關檔案。看到實作的程式碼如下
void MyWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
  qDebug( "mouseDoubleClickEvent" );
}

void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
  qDebug( "mouseMoveEvent" );
}

void MyWidget::mousePressEvent(QMouseEvent *event)
{
  qDebug( "mousePressEvent" );
  //
  switch( event->button() )
  {
  case Qt::LeftButton:
    qDebug( "Left button clicked" );
    break;
  case Qt::MidButton:
    qDebug( "Middle button clicked" );
    break;
  case Qt::RightButton:
    qDebug( "Right button clicked" );
    break;
  default:
    qDebug( "Unknown button clicked" );
    break;
  }
  //
  qDebug( "PoxX:%ld PosY:%ld" ,event->x() , event->y() );
}

void MyWidget::mouseReleaseEvent(QMouseEvent *event)
{
  qDebug( "mouseReleaseEvent" );
}

void MyWidget::wheelEvent(QWheelEvent *event)
{
  qDebug( "wheelEvent" );
  qDebug( "wheel:%ld" , event->delta() );

}

滑鼠的事件都會把詳細資訊記錄在 QMouseEvent 裡,如是滑鼠左鍵或滑鼠右鍵來觸發該事件,看到 MousePressEvent() ,可以透過 QMouseEvent::button() 來取得是哪個按鍵來處理,並可透過 QMouseEvent::x() 與 QMouseEvent::y() 來取得觸發事件的位置, mouseDoubleClickEvent() 、 mouseMoveEvent()  與 mouseReleaseEvent() 處理方法類似所以就只單純顯示偵錯訊息來知道觸發事件的時機。滑鼠滾輪的事件會把詳細資訊記錄在 QWheelEvent 裡,可以透過 QWheelEvent::delta() 來取得上滾或下滾的數值,這個數值基本上都是以 120 為單位,上跟下滾是用數值的正負來表達。

  最後來說一下滾輪事件的奇怪現象,在範例看到的數值不是 120 就是 -120 ,但我在另一個比較複雜的專案使用同樣的方法來取得滾輪數值時可以看到 240 或 360 之類的數值,也許你會很直覺的認定這是滾輪速度的差別,但經我實驗發現同樣的滾動在本範例會送多次的 wheelEvent()
,數值都是 120 ,但在複雜的範例卻發動少次 wheelEvent() ,但有些數值是 240 或 360  ,所以就這實驗來看,其實看到大於 120 數值所代表的是 Qt 的事件觸發時機比較慢所造成滾輪數值會比較大。

2020年9月15日 星期二

在 Qt 環境使用 JavaScript

在 Qt 環境使用 JavaScript

前言

  Qt 提供使用 JavaScript 的環境,但需要經過一些設定才能使用,為了怕日後忘記怎麼設定,在此作個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/Basic,執行結果如下
範例的執行結果

範例的輸入對話框可以輸入 JavaScript 的程式碼,按下右下的"Run"按鈕執行程式。在開始看程式碼之前要先看到專案檔,在開頭可以看到以下
QT       += core gui qml

紅字的部分是要自己加的,這就是使用 JavaScript 的前置作業。

  看到程式碼宣告的部分,如下
#include <QMainWindow>
//
#include <QJSEngine>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
  Q_OBJECT

public:
  MainWindow(QWidget *parent = nullptr);
  ~MainWindow();

protected slots:
  void onPushButtonClicked(bool clicked);
private:
  Ui::MainWindow *ui;
  //
  QJSEngine m_cJSEngine;
};

Qt 要使用 JavaScript 是透過 QJSEngine 這個類別來處理,也就是成員"m_cJSEngine",在使用這個類別需要先 include "QJSEngine"。範例會需要使用到  TextEdit ,如果不熟悉如何使用可以參考 TextEdit 的基本應用。接著就看到實作的部分,看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->pushButton , &QPushButton::clicked , this , &MainWindow::onPushButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  ui->textEdit->setText( tr("console.log('Hello world')") );
}

開頭先綁定"Run"按鈕的事件,接著會喚起 QJSEngine::installExtensions() ,這行可以讓 QJSEngine 支援 console 相關的 Funciton ,最後是設定 TextEdit 給予預設的程式碼,接著來看按鈕事件的程式碼,如下
void MainWindow::onPushButtonClicked(bool clicked)
{
  QJSValue result = m_cJSEngine.evaluate( ui->textEdit->toPlainText() );
  if( result.isError() )
  {
    //
    QJSValue errorStr = result.property( tr("stack") );
    //Filter format string "stack:@:"
    QString errorLine = errorStr.toString().mid(8);
    //
    qDebug( "Error at line(%s) %s" , errorLine.toLocal8Bit().data() ,result.toString().toLocal8Bit().data() );
  }

}

開頭會透過 QJSEngine::evaluate() 來執行結果,引數是目前 TextEdit 的內容,這就幾乎等同在 JavaScript 裡喚起 eval() ,看到回傳的"result",類別為 QJSValue ,其實就 JavaScript 的變數,範例透過喚起 QJSValue::isError() 來確定是否發生 Exception ,接著取出"stack"這個 Property 
,取出來的變數類別依舊是 QJSValue ,這個變數會用來取得當 Exception 發生的行數,但他有一點不完美,它的內容是一個格式的文字,所以透過 mid() 來過濾掉開頭的格式文字,最後過濾完的行樹就會存在"errorLine",最後顯示偵錯訊息的部分,行數是"errorLine",而錯誤的訊息就直接存在"result",所以透過 QJSValue::toString() 就可以把錯誤訊息拿出來。

參考資料


相關文章與資料

2020年9月8日 星期二

在 Qt 使用 Direct3D10 來繪圖

在 Qt 使用 Direct3D10 來繪圖

前言

  在之前的 特製 Widget 裡有提到如何特製一個 Widget ,利用 Qt 提供的繪圖來繪製,如果要用 Direct3D 來繪圖可以嗎?答案是肯定的,在 [ github.com ] QtDirect3D 裡有提供完整的範例,但由於該專案的使用習慣跟我的習慣不太一樣,所以我將參照了該專案的範例做了一個範例,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/CustomDirect3D10Widegt,執行結果如下
範例的執行結果

畫面上的藍色區塊是用 Direct3D10 來繪製的一個特製的 Widget ,如果不熟悉如何特製 Widget 可以參考 特製 Widget 。

  為何使用 Direct3D10 ?這個理由只是因為 Direct3D10 的初始化流程較為簡單,可以用比較少的程式碼看到結果,方便於理解 Qt 是如何跟 Direct3D 合併使用。

  接著來看到特製的 Widget 的部分,宣告的程式碼如下
#include <QWidget>
#include <QTimer>
#include <QEvent>
//
#include <exception>
//
#include <d3d10.h>
#include <DirectXMath.h>
#include <D3Dcompiler.h>

class Direct3D10Widget : public QWidget
{
  Q_OBJECT
public:
  explicit Direct3D10Widget(QWidget *parent = nullptr);
  ~Direct3D10Widget();
  void release();
private:
  bool event(QEvent * event) override;
  void showEvent(QShowEvent * event) override;
  QPaintEngine *paintEngine() const override;
  void paintEvent(QPaintEvent * event) override;
  void resizeEvent(QResizeEvent * event) override;
signals:
  void widgetResized();
  void rendered();
  void d3dReady();
private Q_SLOTS:
  void onFrame();
  void onReset();
private:
  bool m_bIsTimerStart;
  QTimer m_cFrameTimer;
  //
  ID3D10Device* m_pDevice;
  IDXGISwapChain* m_pSwapChain;
  ID3D10RenderTargetView* m_pRTView;

};

比上次的 特製 Widget 還要複雜不少,這次需要一個  Timer 來做為畫面的刷新基礎,這部分不會太難理解,就是每隔一段時間觸發事件來繪圖,也就是"m_cFrameTimer", Direct3D 的初始化整個程式只需做一次,會在某個事件裡做檢查,所以需要"m_bIsTimerStart",剩下的三個成員就是 Direct3D 的相關成員。

  實作的部分先看到 Direct3DWidget::Direct3DWidget() ,程式碼如下
Direct3D10Widget::Direct3D10Widget(QWidget *parent) :
  QWidget(parent),
  m_bIsTimerStart(false),
  m_pDevice(nullptr),
  m_pSwapChain(nullptr),
  m_pRTView(nullptr)
{
  QPalette pal = palette();
  pal.setColor( QPalette::Background, Qt::black );
  setAutoFillBackground( true );
  setPalette( pal );
  //
  setFocusPolicy( Qt::StrongFocus );
  setAttribute( Qt::WA_NativeWindow );
  //
  setAttribute( Qt::WA_PaintOnScreen );
  setAttribute( Qt::WA_NoSystemBackground );
}

這裡設定 Widget 的預設的顏色與必要的屬性,接著看到 Direct3DWidget::showEvent(),程式碼如下
void Direct3D10Widget::showEvent(QShowEvent *event)
{
  if(!m_bIsTimerStart)
  {
    //
    DXGI_SWAP_CHAIN_DESC sd = {};
    sd.BufferCount = 2;
    sd.BufferDesc.Width = width();
    sd.BufferDesc.Height = height();
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = reinterpret_cast<HWND>(winId());
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;
    sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;

    UINT iCreateFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    iCreateFlags |= D3D10_CREATE_DEVICE_DEBUG;
#endif
    auto hr = D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_HARDWARE,
                                         NULL, iCreateFlags,
                                         D3D10_SDK_VERSION, &sd,
                                         &m_pSwapChain, &m_pDevice);
    if (FAILED(hr))
    {
      throw std::runtime_error("D3D10CreateDeviceAndSwapChain() has error");
    }
    //
    onReset();
    emit d3dReady();
    //
    connect(&m_cFrameTimer, &QTimer::timeout, this, &Direct3D10Widget::onFrame);
    m_cFrameTimer.start(16);
    m_bIsTimerStart=true;
  }
  //
  QWidget::showEvent(event);
}

這個事件會在 Qt 需要更新畫面時啟動, Diretc3D 的初始化就放在這裡,但由於這個事件不只觸發一次,所以需要"m_bIsTimerStart"來輔助, Direct3D 初始化完後會喚起 onRest() ,這個 Function 的功用待會再說明,接著觸發 d3dReady() ,這是給使用 Widget 時綁定的事件,接著就綁定 Timer 的事件,並透過 start() 來啟動。看到 Direct3DWidget::resizeEvent() ,程式碼如下
void Direct3D10Widget::resizeEvent(QResizeEvent *event)
{
  if( m_bIsTimerStart )
  {
    onReset();
    emit widgetResized();
  }
  //
  QWidget::resizeEvent( event );
}

這個事件會發生在 Widget 的大小改變時,一樣會喚起 onResst() ,並觸發 widgetResized()。接著看到 Direct3DWidget::onFrame() ,這個 Function 會在 Timer 觸發,程式碼如下
void Direct3D10Widget::onFrame()
{
  //
  const float clearColor[ 4 ] = { 0.0f , 0.0f , 1.0f , 1.0f  };
  m_pDevice->OMSetRenderTargets( 1, &m_pRTView, nullptr );
  m_pDevice->ClearRenderTargetView( m_pRTView , clearColor );
  //
  emit rendered();
  //
  if ( FAILED ( m_pSwapChain->Present( 1, 0 ) ) )
  {
    onReset();
  }
}

這裡就是 Direct3D 常見的把畫面清除成指定顏色的程式碼,並且觸發 rendered() ,當 Present() 失敗時喚起 onReset()。最後看到之前一直沒解釋的 Direct3DWidget::onReset() ,程式碼如下
void Direct3D10Widget::onReset()
{
  ID3D10Texture2D* pBackBuffer;
  if( m_pRTView != nullptr )
  {
    m_pRTView->Release();
    m_pRTView = nullptr;
  }

  auto hr = m_pSwapChain->ResizeBuffers( 0, width() , height() , DXGI_FORMAT_UNKNOWN , 0 );
  if (FAILED( hr ) )
  {
    throw std::runtime_error("ResizeBuffers() has error");
  }
  hr = m_pSwapChain->GetBuffer( 0, IID_PPV_ARGS( &pBackBuffer ) );
  if (FAILED( hr ) )
  {
    throw std::runtime_error("GetBuffer() has error");
  }
  hr = m_pDevice->CreateRenderTargetView( pBackBuffer , nullptr , &m_pRTView );
  if (FAILED( hr ) )
  {
    throw std::runtime_error("CreateRenderTargetView() has error");
  }
  //
  if( pBackBuffer != nullptr )
  {
    pBackBuffer->Release();
    pBackBuffer = nullptr;
  }
  //
  qDebug("wiget resize");
}

這個部分是重新製作一塊跟目前 Widget 一樣大小的繪圖緩衝區,這個部分在一般的 Direct3D 範例是不會這樣用,因為範例幾乎都是固定大小的視窗,所以這邊的處理我是第一次看到,因為做成 Widget ,難免需要改變大小,所以這裡我個人認為是最重要的部分。

  最後一個小提醒,由於 Direct3D 只能在 Windows 平台工作,所以 Qt 專案的編譯器請選擇 MSVC ,由於選擇 MSVC ,所以可以用特別的方式鏈結(Link),如下
#pragma comment(lib, "d3d10.lib")

這個就是鏈結(Link)"d3d10.lib",但這個用法只能用在 MSVC ,正統的作法應該是在 Qt 的專案檔搭配平台的參數來鏈結(Link),如不清楚請參考 Qt 的專案檔 。

參考資料


相關文章與資料

2020年9月1日 星期二

在 TreeView 使用右鍵選單

在 TreeView 使用右鍵選單

前言

  在之前的 TreeView 的基本應用 裡簡單的操作 TreeView ,但 TreeView 很容易需要可以對選取的節點做操作,這個時候就需要使用到右鍵選單,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/TreeView/CustomMenu,執行結果如下
範例的執行結果

範例會產生基本的樹節點,在選取節點並按下右鍵可以跳出選單。

  接著來看看程式的操作,先看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_pTreeViewCustomMenu( NULL )
{
  ui->setupUi(this);
  //
  QStandardItemModel* pModel = new QStandardItemModel(ui->treeView);
  pModel->setHorizontalHeaderLabels( QStringList()<<QStringLiteral("Name")<<QStringLiteral("Comment") );
  QStandardItem* pItemRoot1 = new QStandardItem(QStringLiteral("root1") );
  //first
  pItemRoot1->setChild( 0 ,0 ,new QStandardItem(QStringLiteral("child") ) );
  pItemRoot1->setChild( 0 ,1 ,new QStandardItem(QStringLiteral("I'm child") ) );
  //
  pModel->appendRow( pItemRoot1 );
  //
  ui->treeView->setModel( pModel );

  //
  ui->treeView->setContextMenuPolicy( Qt::CustomContextMenu );

  //
  connect( ui->treeView , &QTreeView::customContextMenuRequested , this , &MainWindow::onTreeViewCustomMenu );

  //Set up custom menu
  m_pTreeViewCustomMenu = new QMenu( this );
  QAction* pAct1 = new QAction( "Action 1" , this );
  connect( pAct1 , &QAction::triggered , this , &MainWindow::onTreeViewAction1Trigger );
  m_pTreeViewCustomMenu->addAction( pAct1 );
  QAction* pAct2 = new QAction( "Action 2" , this );
  connect( pAct2 , &QAction::triggered , this , &MainWindow::onTreeViewAction2Trigger );
  m_pTreeViewCustomMenu->addAction( pAct2 );
  QAction* pAct3 = new QAction( "Action 3" , this );
  connect( pAct3 , &QAction::triggered , this , &MainWindow::onTreeViewAction3Trigger );
  m_pTreeViewCustomMenu->addAction( pAct3 );
}

先看到初始化的部分,裡面有個成員"m_pTreeViewCustomMenu",這是一個型別為"QMenu"的指標,這個類別其實就是 Qt 用來處理右鍵選單的類別,接著會程式開始製造樹的節點並綁定到 TreeView ,然後這點很重要, TreeView 能否對右鍵有反應是需要經過設定,預設值沒辦法,所以透過"setContextMenuPolicy()"來設定,設為後還要綁定事件,讓它可以在右鍵點選後觸發事件,最後的部分是對"m_pTreeViewCustomMenu"做初始化的動作,用法類似樹的結構,每個節點都是"QAction",範例連續產生 3 個"QAction",並個別對其綁定事件來觸發行為。

  接著來看事件的實作,程式碼如下
void MainWindow::onTreeViewCustomMenu( const QPoint& point )
{
  //
  m_pTreeViewCustomMenu->popup( ui->treeView->viewport()->mapToGlobal( point ) );
}

void MainWindow::onTreeViewAction1Trigger()
{
  qDebug( "Action1 triggered" );
}

void MainWindow::onTreeViewAction2Trigger()
{
  qDebug( "Action2 triggered" );
}
void MainWindow::onTreeViewAction3Trigger()
{
  qDebug( "Action3 triggered" );
}

在"onTreeViewCustomMenu()"裡直接透過"popup()"來顯示選單,事件本身雖然會傳送一個位址過來,但該位址並不能直接用,要經過"mapToGlobal()"轉換才能使用,"QAction"的事件的部分就單純地為個別顯示偵錯訊息,如果需要知道目前選取的節點可以參考 TreeView 的基本應用 裡的按鈕事件。

參考資料

[ doc.qt.io ] QTreeView Class
[ doc.qt.io ] QMenu Class

相關文章與資料

[ GitLab ] HelloQt
TreeView 的基本應用

2020年8月25日 星期二

TreeView 的基本應用

TreeView 的基本應用

前言

  在許多遊戲編輯器都需要顯示樹狀資料結構,像是物件瀏覽器或是檔案瀏覽器,Qt 提供 TreeView 來顯示樹狀的資料結構,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/TreeView/Basic,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

圖中左側有 TreeView 在工具箱的位置。執行結果如下
範例的執行結果

範例會加入兩個預設的節點,可以透過"Add root item"、"Add child item"與"Remove"三個按鈕來控制樹的節點。TreeView 不提供在設計介面裡編輯節點的功能,資料必須透過程式綁定資料才能使用。

  接著來看看程式的操作,先看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  QStandardItemModel* pModel = new QStandardItemModel(ui->treeView);
  pModel->setHorizontalHeaderLabels( QStringList()<<QStringLiteral( "Name" )<<QStringLiteral( "Comment" ) );
  QStandardItem* pItemRoot1 = new QStandardItem( QStringLiteral( "root1" ) );
  //first
  pItemRoot1->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
  pItemRoot1->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
  //second
  pItemRoot1->setChild( 1 , 0 , new QStandardItem( QStringLiteral( "child1" ) ) );
  //
  pModel->appendRow( pItemRoot1 );

  //
  QStandardItem* pItemRoot2 = new QStandardItem( QStringLiteral( "root2" ) );
  pItemRoot2->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
  pItemRoot2->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
  //
  pModel->appendRow( pItemRoot2 );

  //
  ui->treeView->setModel( pModel );
  //event bind
  connect( ui->addRootItemButton , &QPushButton::clicked , this , &MainWindow::onAddRootItemButtonClicked );
  connect( ui->addChildItemButton , &QPushButton::clicked , this , &MainWindow::onAddChildItemButtonClicked );
  connect( ui->removeButton , &QPushButton::clicked , this , &MainWindow::onRemoveButtonClicked );
}

程式的開頭會產生"QStandardItemModel",這個類別負責儲存樹的節點資料,透過"setHorizontalHeaderLabels()"來新增欄位名稱,接著是新增節點,節點的類別是"QStandardItem",透過"setChild()"來新增子節點,最後透過"appendRow()"加到"QStandardItemModel "裡,"root2"節點如"root1"節點一樣就不再解釋,接著透過"setModel"將"QStandardItemModel "綁定到 TreeView ,最後是三個功能按鍵的事件綁定。
三個功能鍵的程式如下
void MainWindow::onAddRootItemButtonClicked( bool enabled )
{
  QStandardItemModel* pModel = (QStandardItemModel*)ui->treeView->model();
  pModel->appendRow( new QStandardItem( QStringLiteral( "add root item" ) ) );
}

void MainWindow::onAddChildItemButtonClicked( bool enabled )
{
  QModelIndex index = ui->treeView->currentIndex();
  QStandardItemModel* pModel = (QStandardItemModel*)ui->treeView->model();
  if( index.isValid() )
  {
    QStandardItem* pSelectedItem = pModel->itemFromIndex( index );
    pSelectedItem->appendRow( new QStandardItem( QStringLiteral( "add child item" ) ) );
  }
}

void MainWindow::onRemoveButtonClicked( bool enabled )
{
  QModelIndex index = ui->treeView->currentIndex();
  QStandardItemModel* pModel = (QStandardItemModel*)ui->treeView->model();
  if(index.isValid() )
  {
    pModel->removeRow( index.row() , index.parent() );
  }
}

先看到"Add root item"的部分, TreeView 可以透過"model()"來取得"QStandardItemModel",透過"appendRow()"就可以直接新增節點在根。"Add child item"的部分因為是在目前選取的節點下新增子節點,所以透過"currentIndex()"取得選擇的節點索引,但要考慮他可能是無效的!因為可能沒有選擇任何節點,只要透過"isValid()"就可以驗證,驗證有效後要透過這個索引來取得"QStandardItem",這個時候用"itemFromIndex()"就可以取得,取得後直接透過"appendRow()"來加到該節點的子節點,最後看到"Remove",和"Add child item"一樣是對當下所選取的節點做操作,取得並驗證有效的索引後可以直接用"removeRow()"來刪除節點。

參考資料

[ doc.qt.io ] QTreeView Class

相關文章與資料

[ GitLab ] HelloQt

2020年8月18日 星期二

在 Qt 的顯示中文偵錯訊息

在 Qt 的顯示中文偵錯訊息

前言

  在之前的 TextEdit 的基本應用 裡有使用 TextEdit 來顯示輸入的文字在偵錯訊息,但如果輸入中文會顯示亂碼,這次要解決這個問題。

內容

  這次使用到的範例可以到 [ GitLab ] HelloQt 下載範例,專案路徑
(HelloQt' directory)/TextEdit/Basic ,在範例裡顯示偵錯訊使用以下
void MainWindow::onPushButtonClicked(bool clicked)
{
  qDebug("TextEdit:%s",ui->textEdit->toPlainText().toLocal8Bit().data() );
}

如果 TextEdit 裡有中文的話會顯示亂碼,要解決這個問題可以透過 QDebug ,將範例改成以下
void MainWindow::onPushButtonClicked(bool clicked)
{
  //qDebug("TextEdit:%s",ui->textEdit->toPlainText().toLocal8Bit().data() );
  qDebug() << ui->textEdit->toPlainText();
}

使用的方法類似 C++ STL 字串,要使用時需要另外的 include ,如下
#include <QDebug>

參考資料

[ doc.qt.io ] QDebug Class

相關文章與資料

[ GitLab ] HelloQt
TextEdit 的基本應用

2020年8月10日 星期一

ProgressBar 的基本應用

ProgressBar 的基本應用

前言

  應用程式常常需要顯示進度, Qt 提供 ProgressBar 來解決,這次會說明在 UI 編輯器裡編輯與用程式來編輯的作法,在此做個紀錄。

內容

    先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/ProgressBar/Basic,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

圖中左側標示 ProgressBar 在工具箱的位置。執行結果如下
範例的執行結果

範例會在程式開啟時設定 ProgressBar 為 85% ,由於有綁定事件,當數值被設定後會顯示相關偵錯訊息。接著看到 ProgressBar 的屬性,如下圖
ProgressBar 的屬性

注意"minimum"、"maximum"與"value",這三個屬性會決定最後顯示的百分比,算法是"value" / ("maximum" -  "minimum")。

在程式控制方面,看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->progressBar , &QProgressBar::valueChanged , this , &MainWindow::onProgressBarValueChanged );
  //
  ui->progressBar->setValue(851);
}

程式的開頭會綁定事件,接著將"value"設成851,由於"minimum"與"maximum"分別為 0 與 1000 ,所以依據算法會得到 85% ,由於採用"int"為資料型態,所以小數以後會自動捨去,接著看到事件的部分
void MainWindow::onProgressBarValueChanged(int value)
{
  qDebug( "New value:%ld" , ui->progressBar->value() );
  //
  int newPersent =  ( ( ui->progressBar->value() - ui->progressBar->minimum() ) * 100) / (ui->progressBar->maximum() - ui->progressBar->minimum() );
  qDebug( "New persent:%ld" , newPersent );
}

"value"的部分很簡單直接透 value() 提取即可,但如果要拿出百分比的話就比較麻煩,因為 QProgressBar 不提供直接提取百分比,所以就只能自己算,算法雖然簡單,但程式很容易冗長,可以的話就寫成一個 function。

參考資料

[ doc.qt.io ] QProgressBar Class

相關文章與資料

[ GitLab ] HelloQt

2020年8月3日 星期一

特製 Widget

特製 Widget

前言

  Qt 的預設元件雖然很夠用,但有時需要自己特製 Widget ,該如何特製呢?這次會示範特製一個 Widget 並自制繪圖的部分,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/Basic,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

圖中左側有這次會用的"Widget"在工具箱的位置。執行的結果如下
範例的執行結果


執行的結果會顯示一個黑色的方形,這正是範例特製的 Widget 。

  Qt 要特製 Widget  時事透過繼承"QWidget"這個類別來完成特製,所以要特製就要先新增一個繼承"QWidget"的類別,可以的話請透過精靈來加,步驟如下
新增檔案或專案

在工具列按下"新增檔案或專案",接著如下
新增類別檔案

如圖順序操作即可,接著如下
新增類別精靈

首先要先對類別命名,第二步驟選擇"Base class"為"QWidget",就著按下一步會問檔案受否需要版本控制,然後就新增完畢,範例已經新增"mywidget"這個類別,可以在檔案列表看到,如下
新增的類別檔案

只是新增類別只是可以在程式裡使用該類別,可以在設計介面拖拉出該 Widget 嗎?答案是可以的,在設計介面的工具箱拖拉一個Widget,拖好後在上面按下右鍵,如下
提升類別

按下"提升到...",接著看到以下
選擇要提升的類別

範例以新增好"mywidget"在圖中上方,如果要新增一個新的可以在下方"提升的類別名稱"輸入類別的名稱後按下右側的新增,就可以在上方看到新的 Widget ,選擇好要提升的 Widget 後再按下下方的"提升",就完成指名的特製 Widget,提升完後可以看到類別的名稱的改變,如下圖
提升後的類別名稱

  在新增完類別與在設計介面裡提升後,程式的部分要完成繪製一個黑色的方塊在 Widget 上,先看到 mywidget 的 header ,如下
#include <QWidget>

#include <QPainter>
class mywidget : public QWidget
{
  Q_OBJECT
public:
  explicit mywidget(QWidget *parent = nullptr);

protected:
  void paintEvent(QPaintEvent *event) override;
signals:

};

紅色的部分為手動新增的部分,由於要繪製,所以要覆寫 paintEvent() ,接著看實作的部分,如下
#include "mywidget.h"

mywidget::mywidget(QWidget *parent) : QWidget(parent)
{

}

void mywidget::paintEvent(QPaintEvent *event)
{
  QPainter painter( this );
  QPen pen( Qt::black );
  pen.setWidth( 4 );
  painter.setPen( pen );
  QRect rc( 4, 4, 112, 112 );
  painter.drawRect( rc );

}

紅色的部分為手動新增的部分,Qt  提供 QPainter 來處理繪圖的部分, QPen 則是筆刷,程式設寬度為"4",藉著設定到 QPianter 就可以控制方塊的線寬,接著命令 QPainter 繪製方塊就完成繪製。

參考資料

[ doc.qt.io ] QWidget Class

相關文章與資料

[ GitLab ] HelloQt

2020年7月27日 星期一

DateTimeEdit 的基本應用

DateTimeEdit 的基本應用

前言

  在應用程式中有時會需要日期與時間的輸入, Qt 提供了 DateTimeEdit 來解決輸入日期與時間,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/DateTimeEdit/Basic,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

圖中左側有 DateTimeEdit 在工具箱的位置,執行結果如下
範例的執行結果


範例會有兩個 DateTimeEdit ,上方使用 12 小時的方式來顯示,下方使用 24 小時的方式來顯示,可以透過 DateTimeEdit  的屬性來改變, DateTimeEdit  的屬性如下圖
DateTimeEdit 的屬性

如圖所標示的"displayFormat",透過改變該屬性就可以改變顯示的格式,顯示格式可參考下圖
官方網站的格式說明截圖

接著看程式的操作,看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->dateTimeEdit1 , &QDateTimeEdit::dateTimeChanged , this , &MainWindow::onDateTimeEdit1dateTimeChanged );
  connect( ui->dateTimeEdit2 , &QDateTimeEdit::dateTimeChanged , this , &MainWindow::onDateTimeEdit2dateTimeChanged );

  //
  ui->dateTimeEdit1->setDate( QDate( 2020 , 6 , 6 ) );
  ui->dateTimeEdit1->setTime( QTime( 18 , 30 ) );
}

程式的開頭對兩個 DateTimeEdit 做事件綁定,接著會對上方的 DateTimeEdit 直接對 Date (日期) 與 time (時間) 做輸入,分別透過 setDate() 與 setTime() ,參數很直覺就不多做說明,事件的程式碼如下
void MainWindow::onDateTimeEdit1dateTimeChanged(const QDateTime &datetime)
{
  int y,m,d;
  ui->dateTimeEdit1->date().getDate(&y,&m,&d);
  qDebug( "New dateTimeEdit1's date:%ld_%ld_%ld" , y , m , d);
  //
  qDebug( "New dateTimeEdit1's time:%ld_%ld_%ld" ,
    ui->dateTimeEdit1->time().hour(),
    ui->dateTimeEdit1->time().minute(),
    ui->dateTimeEdit1->time().second() );

}

void MainWindow::onDateTimeEdit2dateTimeChanged(const QDateTime &datetime)
{
  int y,m,d;
  ui->dateTimeEdit2->date().getDate(&y,&m,&d);
  qDebug( "New dateTimeEdit2's date:%ld_%ld_%ld" , y , m , d);
  //
  qDebug( "New dateTimeEdit2's time:%ld_%ld_%ld" ,
    ui->dateTimeEdit2->time().hour(),
    ui->dateTimeEdit2->time().minute(),
    ui->dateTimeEdit2->time().second() );

}

取得 date 是透過 getDate() 來取得,不過也可以透過 year() 、 month() 與 day() 來個別取得,time 的取得分別透過 hour() 、 minute() 與 second() 來取得,要注意 hour() 內定是 24 小時不管顯示模式是否為 12 小時顯示或是 24 小時顯示。

參考資料

[ doc.qt.io ] QDateTimeEdit Class

相關文章與資料

[ GitLab ] HelloQt

2020年7月21日 星期二

在 SpinBox 使用 float 數值

在 SpinBox 使用 float 數值

前言

  在先前的 SpinBox 的基本應用 裡提到了 SpinBox 要使用 float 的數值的作法與 Slider 不一樣,這次就來學習如何使用,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/SpinBox/FloatValue,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

SpinBox 要使用 float 的數值的話要使用 DoubleSpinBox ,圖中左側為 DoubleSpinBox 在工具箱的位置。執行結果如下
範例的執行結果

在設計介面看到 DoubleSpinBox  的屬性如下
DoubleSpinBox 的屬性

屬性中的"decimals"表示精度,預設是 2 ,也就是小術後的兩位數,要注意自己輸入的數值也會被精度影響。

  接著看程式的操作,看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->doubleSpinBox , QOverload<double>::of(&QDoubleSpinBox::valueChanged) , this , &MainWindow::onDoubleSpinBoxValueChanged );
  //
  ui->doubleSpinBox->setValue( 50.5 );
}

這個部分的用法和 SpinBox 是一樣的,只是數值型態為 double ,事件的實現如下
void MainWindow::onDoubleSpinBoxValueChanged(double vlaue)
{
  qDebug( "new value:%.2lf" , ui->doubleSpinBox->value() );
}

這個部分的用法也和 SpinBox 一樣就不再說明。

參考資料

[ doc.qt.io ] QDoubleSpinBox

相關文章與資料

[ GitLab ] HelloQt
SpinBox 的基本應用

2020年7月14日 星期二

SpinBox 的基本應用

SpinBox 的基本應用

前言

  在先前的 Slider 的基本應用 裡介紹了 Slider 可以應用於整數的輸入,但如果數值的範圍大時,要拖到指定的數值時會很花時間,這時候 SpinBox 會比較適合,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/SpinBox/Basic,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

圖中左側有 SpinBox 在工具箱的位置,執行結果如下
範例的執行結果

在設計介面可以看到和 Slider 相似的屬性,如下圖
SpinBox 的屬性

用法和 Slider 差不多,這就不多做解說。

  接著看程式的操作,看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->spinBox , QOverload<int>::of( &QSpinBox::valueChanged ), this , &MainWindow::onSpinBoxValueChanged );
  //
  ui->spinBox->setValue( 50 );
  //
}

程式的開頭做了事件綁定,要注意 valueChanged() 綁定要經過轉型的動作,接著透過 setValue() 來設定 SpinBox 的數值,事件的程式碼如下
void MainWindow::onSpinBoxValueChanged(int value)
{
  qDebug( "new value:%ld" , ui->spinBox->value() );
}

透過 value() 來取得當前的數值,也可以從引數的 value 來取得。

  SpinBox 的好處是可以直接受透過輸入來設定數值,用起來也相單的簡單,但對 Float 數值的解決方案和 Slider 不太一樣,下一篇會做個說明。

參考資料

[ doc.qt.io ] QSpinBox Class

相關文章與資料

[ GitLab ] HelloQt
Slider 的基本應用

2020年7月7日 星期二

Slider 的基本應用

Slider 的基本應用

前言

  應用程式常常需要改變數值,如果用文字輸入就需要作容錯的機制,這時就需要 Slider 這個 widget ,這次會說明在 UI 編輯器裡編輯與用程式來編輯的作法,在此做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Slider/Basic,在 Qt Creator 開啟設計介面會看到以下
範例的設計介面

圖中左側有 Slider 在工具箱的位置, Slider 分為 Horizontal slider 與 Vertical slider 。執行結果如下
範例的執行結果

範例有兩個 Slider ,左側的是 Horizontal slider ,值域是 0 ~ 99 ,右側的是 Vertical slider  ,值域是 -100 ~ 99 。在設計介面時,要注意 Vertical slider 的預設方向是由上到下,如果要改變方向可以改變"InvertedControls" ,如下圖
修改 Slider 的屬性

如果要改變值域可以修改"minimun"與"maximun"。

  接著來看看程式的操作,先看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  int value1 = 50;
  ui->horizontalSlider->setValue( value1 );
  ui->value1->setText( QString::number( value1 ) );
  //
  int value2 = 99;
  ui->verticalSlider->setValue( value2 );
  ui->value2->setText( QString::number( value2 ) );
  //
  connect( ui->horizontalSlider , &QSlider::valueChanged , this , &MainWindow::onHorizontalSliderValueChanged );
  connect( ui->verticalSlider , &QSlider::valueChanged , this , &MainWindow::onVerticalSliderValueChanged );
}

程式會在開頭設定 Slider 的數值與同步對應的 Label , Horizontal slider 同步的數值是 50
, Vertical slider  同步的數值則是 99 。接著是綁定 QSlider::valueChnaged () 事件來同步顯示數值,綁法就是一般的事件綁法。事件觸發的範例如下
void MainWindow::onHorizontalSliderValueChanged(int value)
{
  //Sync value to label
  ui->value1->setText( QString::number( ui->horizontalSlider->value() ) );
}

void MainWindow::onVerticalSliderValueChanged(int value)
{
  //Sync value to label
  ui->value2->setText( QString::number( ui->verticalSlider->value() ) );
}

事件觸發時透過 QSlider::value() 來取得目前的數值,接著設定到對應的 Label 就完成同步了,Function 的引數"value"其實就是 Slider 目前的數值,用這個同步也可以。

  Slider 用起來相當簡單,但不幸的只能對應資料型態為"Int",如果要對應數值型態為"float" 的話就要用別的 widget 才行,下次再來介紹。

參考資料

[ doc.qt.io ] QSlider Class

相關文章與資料

[ GitLab ] HelloQt