windows圖形程式設計 學習雜談 之 高效率視窗背景
剛開始學習windows下的圖形程式設計,只會用API建立視窗和最簡單的訊息函式。
總想給視窗畫個背景圖片,那麼就開始吧。程式設計只看不動手是不會提高的。
開始從網上找資料,主要看的是GDI+_SDK參考手冊。看了畫圖片的部分,很簡單的嘛。
做了個最簡單的OnPaint函式,在WM_WM_PAINT訊息處理中呼叫。
void OnPaint(HWND hWnd) { Bitmap bmp(TEXT("i:\\background.jpg")); //用圖片檔案建立Bitmap物件 HDC hdc(GetDC(hWnd)); //取得裝置環境控制代碼 RECT rect; GetClientRect(hWnd, &rect); //獲取視窗座標 UINT clientWidth = rect.right - rect.left; //計算寬度 UINT clientHeight = rect.bottom - rect.top; //計算高度 Graphics gs(hdc); //建立Graphics物件 gs.DrawImage(&bmp, 0, 0, clientWidth, clientHeight); //畫圖 ReleaseDC(hWnd, hdc); //釋放裝置環境控制代碼 }
編譯執行,效果還不錯。好的開始是成功的第一步。
欣喜過後,發現了一些缺點:
1、重繪過程中有可能出現背景顏色的閃現(我的視窗用的是淡綠的眼睛保護色)
2、視窗放大縮小後,圖片比例變形。這怎麼可以,美女變胖妞。(絕對無法容忍)
3、加入一個計時器計算時間,1024X768繪圖的FPS只有25。速度太慢。
好吧,開始優化,又找資料又查MSDN,發現網上幾乎都是很簡單的demo,只能自己想辦法。
折騰了好幾天,最終優化版出爐。
優化在以下幾個方面:
1、圖片要一次讀取,不能每次OnPaint從檔案讀取,效率太低。把檔案讀取的程式碼放在視窗建立的訊息處。
2、讀取圖片用gdi+容易,繪圖用gdi更快。
3、圖片的縮放在WM_SIZE訊息下處理,這部分很花時間,OnPaint只負責畫調整好的,就快多了。
4、新增WM_ERASEBKGND訊息處理,簡單的return 1就可以不讓windows重新整理背景(背景我自己畫)。
5、採用緩衝機制,處理好的圖片是放在記憶體中,OnPaint才刷到螢幕上。
6、OnPaint的時候,不畫全部視窗,只畫無效部分。
7、這些功能函式封裝成一個類,便於使用。
設計了一個BackGroundBmp類,用於建立背景圖片,調整圖片尺寸,畫到視窗中。
Bitmap* m_pbmp; //gdi+ 的Bitmap物件,用於從檔案中讀取各種型別的圖片(gdi只能讀bmp),並且用於縮放。 HBITMAP m_membmp; //gdi的記憶體圖,是m_pbmp縮放處理後產生的。用於螢幕輸出。 int m_width; //記憶體圖寬 int m_height; //記憶體圖高
成員變數很簡單,4個
類的重要函式有3個,分別是
bool Create(const WCHAR* filename);
bool FixSize(HWND hWnd, int cxWidth, int cxHeight);
bool Paint(HDC hdc, const RECT& rect);
核心部分是FixSize,用gdi+物件實現縮放,畫在gdi的記憶體圖中。(完整程式碼最後附上)
簡單解說一下這個函式的部分程式碼:
double xscale = static_cast<double>(m_width) / bmpwidth;
double yscale = static_cast<double>(m_height) / bmpheight;
bmpwidth、bmpheight是原影象的尺寸
m_width、m_height = 視窗客戶區的尺寸
計算2種尺寸之間的比例關係。
下面就是縮放的關鍵程式碼:
double fixscale = xscale > yscale ? xscale : yscale;
Rect rect(bmpwidth * (xscale - fixscale) / 2,
bmpheight * (yscale - fixscale) / 2,
bmpwidth * fixscale,
bmpheight * fixscale);
就是短短的幾行,折騰了我好半天。(沒辦法,我的數學是體育老師教的)
怎麼調整引數都不對,圖片有變形。
後來才發現gdi+的Rect和gdi的RECT後2個引數居然代表的意義不一樣。吐血。
RECT是gdi的一個結構,4個引數表示左上角座標和右下角座標。
Rect是gdi+的一個物件,前2個引數表示左上角的座標,後面1個是寬度,1個是高度。
座標調整好,接下來就簡單了,建立一個記憶體DC,基於這個DC建立Graphics物件,用DrawImage函式把原始影象畫到記憶體圖中,傳入剛才的Rect實現縮放。(程式碼略)
做完FixSize函式,試著執行下,效果非常棒,縮放沒有一點失真。嘿嘿。很滿意。
唯一缺點是速度比較慢(FPS才十幾),好在視窗大小不是經常調整的。
還折騰過StretchBlt縮放圖片,設定HALFTONE或者STRETCH_HALFTONE模式,縮小圖片都有失真。速度是快,質量就差了。
StretchBlt縮放的程式碼在BackGroundBmp類中有實現,被我註釋掉了。(完整程式碼中有包含,可以比較2種縮放方式的差別)
在工程裡面還添加了一個高精度計時器類,從《Visual C++ 2010入門經典》這書上扒拉下來的,很棒的一個小工具。
在每個訊息處理部分添加了計時。
工程專案順利完工,迫不及待的編譯執行。
效果太完美了,影象清晰,縮放不變形,無失真。OnPaint的FPS是一個我沒想到過的數字(嘿嘿,保密,編譯我的原始碼去試試看)。
以下完整原始碼,vs中建立空的win32 Project,把幾個檔案複製進去就行了。注意修改標頭檔案的路徑和圖片的檔名(對話方塊和控制元件什麼的還不會)
//main.cpp
#include <Windows.h>
#include <tchar.h>
#include <GdiPlus.h>
#include <sstream>
#include "include\hrtimer.h"
#include "include\backgroundbmp.h"
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
HRTimer timer;
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
void PrintText(HWND hWnd, const std::basic_string<TCHAR>& string, int xPos, int yPos);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX WindowClass;
static LPCTSTR szAppName = TEXT("myApp");
HWND hWnd;
MSG msg;
GdiplusStartupInput gdiplusstartinput;
ULONG_PTR gdiplustoken;
GdiplusStartup(&gdiplustoken, &gdiplusstartinput, 0);
WindowClass.cbSize = sizeof(WNDCLASSEX);
WindowClass.style = CS_HREDRAW | CS_VREDRAW;
WindowClass.lpfnWndProc = WindowProc;
WindowClass.cbClsExtra = 0;
WindowClass.cbWndExtra = 0;
WindowClass.hInstance = hInstance;
WindowClass.hIcon = LoadIcon(0, IDI_APPLICATION);
WindowClass.hCursor = LoadCursor(0, IDC_ARROW);
WindowClass.hbrBackground =reinterpret_cast<HBRUSH>(COLOR_WINDOW+1);
WindowClass.lpszMenuName = 0;
WindowClass.lpszClassName = szAppName;
WindowClass.hIconSm = 0;
RegisterClassEx(&WindowClass);
hWnd = CreateWindow(szAppName,
TEXT("background"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
nullptr,
nullptr,
hInstance,
nullptr);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
while(GetMessage(&msg, 0, 0, 0) == TRUE)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GdiplusShutdown(gdiplustoken);
return static_cast<int>(msg.wParam);
}
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT paintst;
static int cxclient, cyclient; //客戶區長度和寬度
static std::basic_ostringstream<TCHAR> ss1, ss2, ss3;
static BackGroundBmp bmp;
switch(message)
{
case WM_CREATE:
timer.StartTimer();
bmp.Create(TEXT("i:\\background.jpg"));
ss1.str(TEXT(""));
ss1 << "CreateFPS: " << static_cast<int>(1 / timer.StopTimer());
return 0;
case WM_ERASEBKGND:
return 1;
case WM_SIZE:
timer.StartTimer();
cxclient = LOWORD(lParam);
cyclient = HIWORD(lParam);
bmp.FixSize(hWnd, cxclient, cyclient);
ss2.str(TEXT(""));
ss2 << "SizeFPS: " << static_cast<int>(1 / timer.StopTimer());
return 0;
case WM_PAINT:
hdc = BeginPaint(hWnd, &paintst);
timer.StartTimer();
bmp.Paint(hdc, paintst.rcPaint);
//輸出size和fps
ss3.str(TEXT("")); //清空輸出流
ss3 << TEXT("PaintFPS: ") << static_cast<int>(1 / timer.StopTimer());
PrintText(hWnd, ss1.str(), 0, 0);
PrintText(hWnd, ss2.str(), 0, 20);
PrintText(hWnd, ss3.str(), 0, 40);
ss3.str(TEXT("")); //清空輸出流
ss3 << TEXT("SIZE: ") << cxclient << TEXT(" ") << cyclient;
PrintText(hWnd, ss3.str(), 0, 60);
EndPaint(hWnd, &paintst);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
void PrintText(HWND hWnd, const std::basic_string<TCHAR>& str, int xPos, int yPos)
{
HDC hdc = GetDC(hWnd);
TextOut(hdc, xPos, yPos, str.c_str(), str.size());
ReleaseDC(hWnd, hdc);
}
//backgroundbmp.h
//背景圖片類
//只能用於桌面應用程式,不能在控制檯用,受到gdi+的一些函式限制
#pragma once
#include <windows.h>
#include <tchar.h>
#include <GdiPlus.h>
using namespace Gdiplus;
class BackGroundBmp
{
public:
BackGroundBmp()
: m_pbmp(nullptr), m_membmp(nullptr), m_width(0), m_height(0) {}
BackGroundBmp(const WCHAR* filename)
: m_pbmp(nullptr), m_membmp(nullptr), m_width(0), m_height(0)
{Create(filename);}
virtual ~BackGroundBmp();
bool isOk() const {return m_pbmp != nullptr;}
HBITMAP GetBmp() {return m_membmp;}
int GetBmpWidth() const {return m_width;}
int GetBmpHeight() const {return m_height;}
bool Create(const WCHAR* filename);
bool FixSize(HWND hWnd, int cxWidth, int cxHeight);
bool Paint(HDC hdc, const RECT& rect);
private:
BackGroundBmp(const BackGroundBmp&); //not allow
BackGroundBmp& operator=(const BackGroundBmp&); //not allow
private:
Bitmap* m_pbmp; //Gdi+ object
HBITMAP m_membmp; //gdi object
int m_width;
int m_height;
};
//backgroundbmp.cpp
#include "include\backgroundbmp.h"
BackGroundBmp::~BackGroundBmp()
{
DeleteObject(m_membmp);
}
bool BackGroundBmp::Create(const WCHAR* filename)
{
if (filename == nullptr)
return false;
m_pbmp = Bitmap::FromFile(filename);
return (m_pbmp != nullptr);
}
//效果不好,雖然速度快,縮小有失真
/*
bool BackGroundBmp::FixSize(HWND hWnd, int cxWidth, int cxHeight)
{
if (!m_pbmp)
return false;
m_width = cxWidth;
m_height = cxHeight;
//取得原圖長寬
int bmpwidth = m_pbmp->GetWidth();
int bmpheight = m_pbmp->GetHeight();
//計算x、y方向長寬比例
double xscale = static_cast<double>(m_width) / bmpwidth;
double yscale = static_cast<double>(m_height) / bmpheight;
//計算原圖複製後的長寬,經過同比放大或縮小,截去超過的x部分或y部分,保證圖片長寬比例不變形
double fixscale = xscale > yscale ? xscale : yscale;
//直接drawimage在記憶體中,比上一個版本快很多
//刪除舊的記憶體圖
DeleteObject(m_membmp);
HDC hdc = GetDC(hWnd);
//根據客戶區大小建立新的記憶體圖和記憶體DC
m_membmp = CreateCompatibleBitmap(hdc, m_width, m_height);
HDC destdc = CreateCompatibleDC(hdc);
HDC sourcedc = CreateCompatibleDC(hdc);
//選擇記憶體DC繪圖
HBITMAP tempbmp;
Status status = m_pbmp->GetHBITMAP(Color(255,255,255), &tempbmp);
if (status != Ok)
MessageBox(0,L"wrong",0,0);
HGDIOBJ destobj = SelectObject(destdc, m_membmp);
HGDIOBJ sourceobj = SelectObject(sourcedc, tempbmp);
//Graphics graphics(memdc);
//最佳影象質量
//graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
SetStretchBltMode(sourcedc,STRETCH_HALFTONE);
SetBrushOrgEx(sourcedc,0, 0, nullptr);
//直接畫到記憶體DC中
//graphics.DrawImage(m_pbmp, rect, 0, 0, bmpwidth, bmpheight, UnitPixel);
StretchBlt(destdc,
bmpwidth * (xscale - fixscale) / 2,
bmpheight * (yscale - fixscale) / 2,
bmpwidth * fixscale,
bmpheight * fixscale,
sourcedc,
0,
0,
bmpwidth,
bmpheight,
SRCCOPY);
SelectObject(sourcedc, sourceobj);
DeleteDC(sourcedc);
SelectObject(destdc, destobj);
DeleteDC(destdc);
ReleaseDC(hWnd, hdc);
}
*/
//速度慢,效果很好,無失真
bool BackGroundBmp::FixSize(HWND hWnd, int cxWidth, int cxHeight)
{
//如果影象沒有從檔案載入,生成1個空的客戶區大小的記憶體影象
HDC hdc = GetDC(hWnd);
m_width = cxWidth;
m_height = cxHeight;
//刪除舊的記憶體圖
DeleteObject(m_membmp);
//根據客戶區大小建立新的記憶體圖和記憶體DC
m_membmp = CreateCompatibleBitmap(hdc, m_width, m_height);
if (!m_pbmp)
{
ReleaseDC(hWnd, hdc);
return false;
}
//取得原圖長寬
int bmpwidth = m_pbmp->GetWidth();
int bmpheight = m_pbmp->GetHeight();
//計算x、y方向長寬比例
double xscale = static_cast<double>(m_width) / bmpwidth;
double yscale = static_cast<double>(m_height) / bmpheight;
//計算原圖複製後的長寬,經過同比放大或縮小,截去超過的x部分或y部分,保證圖片長寬比例不變形
double fixscale = xscale > yscale ? xscale : yscale;
Rect rect(bmpwidth * (xscale - fixscale) / 2,
bmpheight * (yscale - fixscale) / 2,
bmpwidth * fixscale,
bmpheight * fixscale);
HDC memdc = CreateCompatibleDC(hdc);
//選擇記憶體DC繪圖
HGDIOBJ oldmap = SelectObject(memdc, m_membmp);
Graphics graphics(memdc);
//最佳影象質量
graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
//直接畫到記憶體DC中
graphics.DrawImage(m_pbmp, rect, 0, 0, bmpwidth, bmpheight, UnitPixel);
SelectObject(memdc, oldmap);
DeleteDC(memdc);
ReleaseDC(hWnd, hdc);
return true;
}
bool BackGroundBmp::Paint(HDC hdc, const RECT& rect)
{
HDC memdc = CreateCompatibleDC(hdc);
HGDIOBJ oldbmp = SelectObject(memdc, m_membmp);
BitBlt(hdc,
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
memdc,
rect.left, rect.top,
SRCCOPY);
SelectObject(memdc, oldbmp);
DeleteDC(memdc);
return true;
}
//hrtimer.h
//高精度計時器類
#pragma once
#include <Windows.h>
class HRTimer
{
public:
HRTimer(): frequency(1.0 / GetFrequency()) {}
double GetFrequency()
{
LARGE_INTEGER proc_freq;
::QueryPerformanceFrequency(&proc_freq);
return static_cast<double>(proc_freq.QuadPart);
}
void StartTimer()
{
DWORD_PTR oldmask = ::SetThreadAffinityMask(::GetCurrentThread(), 0);
::QueryPerformanceCounter(&start);
::SetThreadAffinityMask(::GetCurrentThread(), oldmask);
}
double StopTimer()
{
DWORD_PTR oldmask = ::SetThreadAffinityMask(::GetCurrentThread(), 0);
::QueryPerformanceCounter(&stop);
::SetThreadAffinityMask(::GetCurrentThread(), oldmask);
return ((stop.QuadPart - start.QuadPart) * frequency);
}
private:
LARGE_INTEGER start;
LARGE_INTEGER stop;
double frequency;
};