在 Qt 使用 Direct3D10 來繪圖
前言
內容
先到
[ 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 的專案檔 。
參考資料
相關文章與資料