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月22日 星期二

關於 Blender 的 IK 動作不如預期的問題

 關於 Blender 的 IK 動作不如預期的問題

前言

  最近在用 Blender 的時候發現 IK (Inverse Kinematics) 的動作不如預期,但用同樣的調整方式在 Blender 的人骨架卻可以正常動作,所以我做了個實驗,在此把實驗的過程做個紀錄。


內容

  為了實驗,我拉了個能重現問題的骨架,骨架如下圖

有問題的骨架


動起來會如下圖

IK (Inverse Kinematics) 動作不正常


圖中不論如何調整 Pole Angle 都無法正常動作,經過一段實驗發現原來骨架一定要是"彎"

的才能讓 IK (Inverse Kinematics)  正常動作,如下圖

調整後的骨架


動作結果就正常,如下圖

動作正常的骨架


  Pole Target 在 Rig 時難以避免使用,由於我的 Rig 是看入門教學學來的,入門教學通常不會教骨架不能是直的,不然動作會不正常,這次的經驗學到了實務經驗的重要。


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 的基本應用