Windows GDI 教程(一) 一个简单的绘图程序

常见的图形编程库,除了 GDI 外还有 GDI+、OpenGL、DirectX等等,GDI 是其中最基础的一个库。所以 GDI 注定了不会有高级应用,有兴趣的就当刷低级怪吧。

在教程的最开始,需要简单的说明一些前置条件。

开发环境与前言

首先是标明开发环境:
操作系统:win7 (xp应该可以,win8未测试)
使用工具:visual studio 2010(或更高)

窗口创建

以前代码的前置问题,首先本教程内的 GDI 画图,在最开始部分主要是在窗口内部绘制(为避免混乱窗口外部,也就是整个桌面的绘制会在很后面的地方讨论)。因此,这里需要对于创建窗口一定的了解。为了让大家可以直接复制完代码就可以在一个文件里面运行,博主准备的代码是手动动态创建窗口的代码,所以这里创建窗口的代码有点长,不过大家不要怕,我们要关注的只是中间的一小部分。这里博主先把代码贴上:

#include <windows.h>

// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";

/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        // 窗口绘制消息
        case WM_PAINT:
            /*
             * 我们只需要在这里调用我们的 GDI 绘制函数就可以,其他地方可以先无视
             */
        break;
        // 窗口关闭消息
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        // 窗口销毁消息
        case WM_DESTROY:
            PostQuitMessage(0); // 发送离开消息给系统
        break;
        // 其他消息
        default:
            // pass 给系统,咱不管
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

/*
 * 第一步,注册窗口类
 */
void RegisterMyWindow(HINSTANCE hInstance)
{
    WNDCLASSEX wc;  

    // 1)配置窗口属性
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = MyWindowProc; // 设置第四步的窗口过程回调函数
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    // 2)注册
    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        exit(0); // 进程退出
    }
}

/*
 * 第二步,创建窗口
 */
HWND CreateMyWindow(HINSTANCE hInstance, int nCmdShow)
{
    HWND hwnd;
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        TEXT("我的窗口名称"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, // 出现坐标 x,y 默认分配 窗口宽 400 高 300
        NULL, NULL, hInstance, NULL);

    if(hwnd == NULL)
    {
        MessageBox(NULL, TEXT("窗口创建失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
        exit(0); // 进程退出
    }

    // 显示窗口
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    return hwnd;
}

/*
 * 主函数
 */
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    HWND hwnd;
    MSG Msg;

    // 第一步,注册窗口类
    RegisterMyWindow(hInstance);
    // 第二步:创建窗口
    hwnd =  CreateMyWindow(hInstance, nCmdShow);
    // 第三步:消息循环
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

运行效果图:
创建一个空白窗口

这个创建窗口的代码很长,看起来有点吓人,但是学习 GDI 的过程中,这其中几乎是完全不需要记忆的,只要有一定的了解,然后会 copy 就可以了。当然如果你能懂也是更好,以上代码的出处为 《Windows SDK 教程(三) 一些细节以及动态创建控件》,有兴趣的可以去看看。

那么博主也说过了,最开始的时候,这段长长的代码其实注意一个地方就可以了,就是其中的第四步窗口过程中的一个小 case。

/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        // 窗口绘制消息
        case WM_PAINT:
            /*
             * 只有这一个 case 是我们 GDI 入门中需要注意的
             *
             * 当程序执行到这个地方的时候,意味着系统像我们的程序发送了 WM_PAINT 消息
             * 也就是告诉我们的程序,可以开始绘制窗口的内容了。
             *
             */
        break;
        // 其余略...
    }
    return 0;
}

这样看来,貌似我们要注意的地方确实很小吧。那么我接着往下走。

PS:默认情况下,系统只会向我们的程序发送一次 WM_PAINT 消息。如果想要再来一次,需要使用 SendMessage 函数,来自己向自己手动发送该消息。

坐标系

GDI 的绘图坐标系与普通的数学坐标系不同,原点 (0,0) 位于左上角。如图:
坐标系

设备上下文(DC)

DC (设备上下文, Device Contexts)是 GDI 编程中一个很基础同时也很重要的概念。博主以前看过不少网上的资料以及书上的描述,总感觉他们说的都很奇怪。这里博主为了方便大家理解就说说自己的看法:

大家只要把 DC 当成一个保存图像的内存对象即可。当我们使用 GDI 提供的函数去操作 DC 的时候,也就意味着在使用函数去修改保存在这块内存上的图像。

BeginPaint 与 EndPaint
用于从目标窗口获取可画图的 DC,以及关闭这个 DC。
函数原型

HDC BeginPaint(
  _In_   HWND hwnd,             // 传入想要获取 DC 的窗口句柄
  _Out_  LPPAINTSTRUCT lpPaint  // 保存目标窗口的绘图信息
);

 BOOL EndPaint(
  _In_  HWND hWnd,                  // 目标窗口的句柄
  _In_  const PAINTSTRUCT *lpPaint  // 目标窗口的绘图信息
);

SelectObject
设置目标 DC 选中指定的对象(如画笔、画刷、图片等等)。
函数原型

HGDIOBJ SelectObject(
  _In_  HDC hdc,        // 目标 DC 的句柄
  _In_  HGDIOBJ hgdiobj // 被选中的对象
);

CreatePen
创建一个画笔(pen)对象。
函数原型

HPEN CreatePen(
  _In_  int fnPenStyle,     // 样式
  _In_  int nWidth,         // 宽度
  _In_  COLORREF crColor    // 颜色
);

MoveToEx
移动绘制的初始位置。未移动则默认是 (0,0)。(C语言基础好的可以联想 fseek 函数)
函数原型

BOOL MoveToEx(
  _In_   HDC hdc,           // 操作目标DC的句柄
  _In_   int X,             // x 坐标
  _In_   int Y,             // y 坐标
  _Out_  LPPOINT lpPoint    // 保存移动后的当前坐标
);

LineTo
使用当前选中的对象(selected object、通常是画笔)从当前位置绘制一条直线到目标位置。
函数原型

BOOL LineTo(
  _In_  HDC hdc,    // 目标DC句柄
  _In_  int nXEnd,  // 目标位置 x 坐标
  _In_  int nYEnd   // 目标位置 y 坐标
);

绘制直线实例

#include <windows.h>

// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";

void Paint(HWND hwnd) 
{
    // paint struct 绘图结构体,存储目标窗口可以绘图的客户端区域(client area)
    PAINTSTRUCT ps;
    HDC hdc;   // DC(可画图的内存对象) 的句柄
    HPEN hpen; // 画笔

    // 通过窗口句柄获取该窗口的 DC
    hdc = BeginPaint(hwnd, &ps);
    // 创建画笔
    hpen = CreatePen(PS_SOLID, 1, RGB(255,0,0));
    // DC 选择画笔
    SelectObject(hdc,hpen);
    // (画笔)从初始点移动到 50,50
    MoveToEx(hdc, 50, 50, NULL);
    // (画笔)从初始点画线到 100,100
    LineTo(hdc, 150, 100);

    EndPaint(hwnd, &ps);
}

/*
 * 第四步,窗口过程
 */
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        // 窗口绘制消息
        case WM_PAINT:
            Paint(hwnd); // 调用我们的 GDI 绘制函数
        break;
        // 其余略...
    }
    return 0;
}

// 其余略

运行效果图:
这里写图片描述

  • 20
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
这是一个比较复杂的项目,需要一定的Python编程经验和Windows API的了解。以下是一个可能的实现: 1. 首先需要导入一些必要的模块: ```python import win32gui import win32con import win32api import ctypes ``` 2. 然后定义一些常量: ```python # 窗口标题 WINDOW_TITLE = "双曲线绘制器" # 窗口大小 WINDOW_WIDTH = 800 WINDOW_HEIGHT = 600 # 绘图区域大小 PLOT_WIDTH = 600 PLOT_HEIGHT = 400 # 缩放比例 SCALE_FACTOR = 1.2 # 移动步长 MOVE_STEP = 20 # 初始缩放和偏移量 INITIAL_SCALE = 1.0 INITIAL_OFFSET_X = 0 INITIAL_OFFSET_Y = 0 # 双曲线参数 A = 100 B = 50 ``` 3. 然后定义主窗口类和消息处理函数: ```python class MainWindow: def __init__(self): self.hwnd = None self.hdc = None self.scale = INITIAL_SCALE self.offset_x = INITIAL_OFFSET_X self.offset_y = INITIAL_OFFSET_Y def create(self): wc = win32gui.WNDCLASS() wc.lpfnWndProc = self._window_proc wc.lpszClassName = "MainWindowClass" wc.hbrBackground = win32gui.GetStockObject(win32con.WHITE_BRUSH) win32gui.RegisterClass(wc) self.hwnd = win32gui.CreateWindow( "MainWindowClass", WINDOW_TITLE, win32con.WS_OVERLAPPEDWINDOW, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, None, None, None, None ) win32gui.ShowWindow(self.hwnd, win32con.SW_SHOWDEFAULT) win32gui.UpdateWindow(self.hwnd) def _window_proc(self, hwnd, msg, wparam, lparam): if msg == win32con.WM_PAINT: self._on_paint() elif msg == win32con.WM_DESTROY: self._on_destroy() elif msg == win32con.WM_MOUSEWHEEL: self._on_mouse_wheel(wparam, lparam) elif msg == win32con.WM_KEYDOWN: self._on_key_down(wparam, lparam) else: return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) return 0 def _on_paint(self): ps = win32gui.PAINTSTRUCT() self.hdc = win32gui.BeginPaint(self.hwnd, ps) # 绘制坐标轴 self._draw_axes() # 绘制双曲线 self._draw_hyperbola() win32gui.EndPaint(self.hwnd, ps) def _on_destroy(self): win32gui.PostQuitMessage(0) def _on_mouse_wheel(self, wparam, lparam): delta = win32api.HIWORD(wparam) if delta > 0: self._zoom_in() else: self._zoom_out() def _on_key_down(self, wparam, lparam): if wparam == win32con.VK_LEFT: self._move_left() elif wparam == win32con.VK_RIGHT: self._move_right() elif wparam == win32con.VK_UP: self._move_up() elif wparam == win32con.VK_DOWN: self._move_down() def _draw_axes(self): pen = win32gui.CreatePen(win32con.PS_SOLID, 1, win32api.RGB(0, 0, 0)) win32gui.SelectObject(self.hdc, pen) # x轴 win32gui.MoveToEx(self.hdc, 0, PLOT_HEIGHT // 2, None) win32gui.LineTo(self.hdc, PLOT_WIDTH, PLOT_HEIGHT // 2) # y轴 win32gui.MoveToEx(self.hdc, PLOT_WIDTH // 2, 0, None) win32gui.LineTo(self.hdc, PLOT_WIDTH // 2, PLOT_HEIGHT) win32gui.DeleteObject(pen) def _draw_hyperbola(self): pen = win32gui.CreatePen(win32con.PS_SOLID, 2, win32api.RGB(255, 0, 0)) win32gui.SelectObject(self.hdc, pen) # 计算双曲线上的点 points = [] for x in range(-PLOT_WIDTH // 2, PLOT_WIDTH // 2): y = self._hyperbola_y(x) x, y = self._transform_point(x, y) points.append((x, y)) # 绘制双曲线 win32gui.Polyline(self.hdc, points) win32gui.DeleteObject(pen) def _hyperbola_y(self, x): return A * (x - self.offset_x) ** 2 / B ** 2 - A def _transform_point(self, x, y): x = int((x - PLOT_WIDTH // 2) * self.scale + PLOT_WIDTH // 2) y = int((y - PLOT_HEIGHT // 2) * self.scale + PLOT_HEIGHT // 2) return x, y def _zoom_in(self): self.scale *= SCALE_FACTOR self._redraw() def _zoom_out(self): self.scale /= SCALE_FACTOR self._redraw() def _move_left(self): self.offset_x -= MOVE_STEP self._redraw() def _move_right(self): self.offset_x += MOVE_STEP self._redraw() def _move_up(self): self.offset_y -= MOVE_STEP self._redraw() def _move_down(self): self.offset_y += MOVE_STEP self._redraw() def _redraw(self): win32gui.InvalidateRect(self.hwnd, None, True) def message_loop(): while True: msg = win32gui.GetMessage(None, 0, 0) if msg == 0: break win32gui.TranslateMessage(msg) win32gui.DispatchMessage(msg) ``` 4. 最后,在`__main__`函数中创建主窗口并进入消息循环: ```python if __name__ == "__main__": main_window = MainWindow() main_window.create() message_loop() ``` 完整代码如下:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值