《Windows 程序设计(第3版)》——6.7 【实例】窗口查看器

简介: 程序的主要功能是显示鼠标所指向窗口的一些信息,如标题、窗口类名和应用程序名称等。其运行效果如图6.7所示。在主窗口左上角矩形框中按下鼠标,光标将变成图中带十字的黑圆圈形状。按住鼠标左键移动这个黑圆圈,窗口查看器会显示出黑圆圈下窗口的属性信息。

本节书摘来自异步社区《Windows 程序设计(第3版)》一书中的第6章,第6.7节,作者:王艳平 , 张铮著,更多章节内容可以访问云栖社区“异步社区”公众号查看

6.7 【实例】窗口查看器

本节将用具体实例让你切身体会对象化程序设计方式,从而感受框架程序提供的近乎完美的程序接口。

程序的主要功能是显示鼠标所指向窗口的一些信息,如标题、窗口类名和应用程序名称等。其运行效果如图6.7所示。在主窗口左上角矩形框中按下鼠标,光标将变成图中带十字的黑圆圈形状。按住鼠标左键移动这个黑圆圈,窗口查看器会显示出黑圆圈下窗口的属性信息。例子程序在配套光盘的06WinLooker工程下。最好先运行此程序熟悉一下它的功能。

screenshot

例子的创建过程是:先创建一个空的Win32 Application类型的工程,工程名设为06WinLooker;工程创建完毕以后,按照上节所述的方法,单击菜单命令“Project/Settings...”修改工程的设置,使它支持MFC;最后新建looker.h和looker.cpp两个文件,以编写下面要讲述的程序代码。

6.7.1 窗口界面
程序06WinLooker中包含4个类,CMyApp(应用程序类)、CMainWindow(主窗口类)、CWindowInfo(管理目标窗口的类)和CMyButton(按钮类)。本小节讲述CMyApp和Cmain Window类,它们主要负责程序的窗口界面。

CMyApp类从应用程序类CWinApp继承,它的作用是初始化应用程序的当前实例(InitInstance)、运行消息循环(Run)和执行程序终止时的清理工作(ExitInstance)。基于框架生成的应用程序必须有且只有一个从CWinApp派生的类的对象。程序启动后,框架会调用此对象的成员函数来初始化和运行应用程序。定义CMyApp类对象的代码在looker.cpp中。

CMyApp theApp;    // 应用程序实例对象
CMyApp类定义在looker.h文件中。

class CMyApp : public CWinApp
{
public:
  virtual BOOL InitInstance();
};

在应用程序主线程初始化时,框架程序将调用虚函数InitInstance给应用程序一个初始化自己的机会。06WinLooker程序将在此函数中创建当前线程的主窗口对象,显示和刷新主窗口。

BOOL CMyApp::InitInstance()    // looker.h文件
{
  m_pMainWnd = new CMainWindow;
  m_pMainWnd->ShowWindow(m_nCmdShow);
  m_pMainWnd->UpdateWindow();
  return TRUE;  // 初始化成功,进入消息循环
}

CMainWindow类从CWnd类继承,一个CMainWindow对象代表着一个窗口,此窗口的外观和行为由CMainWindow类的初始化代码和响应消息的代码决定。整个应用程序只有一个主窗口,所以主线程的InitInstance函数只创建了一个CMainWindow对象,然后要求此对象显示、刷新窗口。CMyApp::InitInstance返回后,框架程序执行theApp对象的Run函数,开始分发主线程消息队列中的消息。

Windows发送给主窗口的消息由框架程序的AfxWndProc函数交给相应的CMainWindow对象处理。CMainWindow对象中包含了主窗口的初始化代码、记录状态信息的数据、特定的消息处理函数,而且还包含了删除自己的代码。下面是实现图6.7所示的主窗口界面所需的代码。

class CMainWindow : public CWnd    // looker.h文件
{
protected:
  int m_cxChar;           // 字符的平均宽度
  int m_cyChar;           // 字符的高
  int m_cyLine;           // 一行字符占用的空间的垂直长度
  
  HCURSOR m_hCursorArrow;      // 通常模式下使用的光标句柄(箭头光标)
  HCURSOR m_hCursorTarget;      // 用户选定窗口时使用的光标句柄(自定义光标)

  RECT m_rcMouseDown;        // 接收鼠标下按的方框的位置坐标
  RECT m_rcMsgBoxBorder;       // 消息框边框的位置坐标
  RECT m_rcMsgBox;          // 消息框的位置坐标
  CPoint m_ptHeaderOrigin;      // 绘制标题的起始位置
  
  BOOL m_bCatchMouseDown;      // 是否捕捉到鼠标下按事件
public:
  CMainWindow();
protected:
  void DrawMouseInput(CDC* pDC);
  void DrawMsg(CDC* pDC);
  void DrawMsgHeader(CDC* pDC);
protected:
  virtual void PostNcDestroy();
  afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
  afx_msg void OnPaint();
  afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
  afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
  afx_msg void OnMouseMove(UINT nFlags, CPoint point);
  DECLARE_MESSAGE_MAP()
};

CMainWindow类的实现代码在looker.cpp文件中,首先是初始化消息映射表。

BEGIN_MESSAGE_MAP(CMainWindow, CWnd)
ON_WM_CREATE()
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()

类的构造函数完成了初始化主窗口的操作,包括窗口类的注册和窗口的创建。

CMainWindow::CMainWindow()
{
  m_bCatchMouseDown = FALSE;

  // 加载两个光标
  m_hCursorArrow = AfxGetApp()->LoadStandardCursor(IDC_ARROW);
  m_hCursorTarget = AfxGetApp()->LoadCursor(IDC_TARGET);

  // 注册窗口类。因为程序中要改变光标的形状,所以不要在窗口类里设置光标,否则每次鼠标移动系统都
  // 会恢复光标,极大地影响运行速度
  LPCTSTR lpszClassName = AfxRegisterWndClass(0, NULL,
      (HBRUSH)(COLOR_3DFACE + 1), AfxGetApp()->LoadIcon(IDI_MAIN));

  // 创建窗口
  CreateEx(0, lpszClassName, "窗口查看器", 
    WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX, 
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL);
}

06WinLooker程序自定义了两个资源,一个是ID为IDC_TARGET的光标,另一个是ID为IDI_MAIN的图标。光标资源是必须添加的,当用户在主窗口左上角正方形区域按下鼠标左键时,程序执行下面的代码设置光标为自定义的形状。

::SetCursor(m_hCursorTarget);      // 设置光标的形状
自定义的光标是带十字的圆圈,如图6.7所示。向工程中添加资源脚本文件后,选择菜单命令“Insert/Resource...”新建一个光标资源,可以照着样子画一个或者直接拷贝配套光盘上的位图。另外,光标资源有一个Hot spot属性,最好将它设置在圆圈的中心。

CreateEx函数执行时,主窗口会接收到WM_CREATE消息,OnCreate函数被调用。在这个函数里,06WinLooker要指定窗口界面中各区域的坐标位置,设置窗口的大小和光标形状。

#define MAX_STRINGS   5
#define IDB_CLOSE     10
int CMainWindow::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  if(CWnd::OnCreate(lpCreateStruct) == -1)
    return -1;

  CClientDC dc(this);

  TEXTMETRIC tm;
  // GetTextMetrics函数取得指定设备环境中字符的大小属性 
  ::GetTextMetrics(dc, &tm);
  m_cxChar = tm.tmAveCharWidth;
  m_cyChar = tm.tmHeight;
  m_cyLine = tm.tmHeight + tm.tmExternalLeading;

  // 设置窗口左上角正方形区域的位置坐标
  ::SetRect(&m_rcMouseDown, 12, 12, 48, 48);

  // 设置标题的起始坐标
  m_ptHeaderOrigin.x = 48 + 6;
  m_ptHeaderOrigin.y = 12 + 4;

  // 设置消息框的位置坐标
  m_rcMsgBoxBorder.left = m_ptHeaderOrigin.x + 8*m_cxChar;
  m_rcMsgBoxBorder.top = 12;
  m_rcMsgBoxBorder.right = m_rcMsgBoxBorder.left + m_cxChar*32 + 8;
  m_rcMsgBoxBorder.bottom = m_rcMsgBoxBorder.top + m_cyLine*MAX_STRINGS + 8;
  m_rcMsgBox = m_rcMsgBoxBorder;
  // inflate是膨胀的意思,InflateRect函数使长方形的宽和高增大或缩小一定的数量
  ::InflateRect(&m_rcMsgBox, -4, -4);
  
  // 创建按钮窗口对象。等设计完CMyButton类后再添加下面两行代码
  // RECT rcButton = {12, m_rcMsgBoxBorder.bottom - 18, 64, m_rcMsgBoxBorder.bottom };
  // new CMyButton("Close", rcButton, this, IDB_CLOSE);
  
  // 设置本窗口的大小
  RECT rect;
  ::SetRect(&rect, 0, 0, m_rcMsgBoxBorder.right + 12, m_rcMsgBoxBorder.bottom + 12);
  // 上面得到的是窗口客户区的大小,AdjustWindowRect将客户区的大小转化成最终窗口的大小
  ::AdjustWindowRect(&rect, GetStyle(), FALSE);
  // 重新设置窗口的大小
  ::SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, rect.right - rect.left, rect.bottom - rect.top,
    SWP_NOMOVE | SWP_NOREDRAW);
  
  // 设置光标形状
  ::SetCursor(m_hCursorArrow);
  return 0;
}

图6.8详细说明了各区域的位置坐标。一般情况下,窗口界面的设置和子窗口的创建都是在响应WM_CREATE消息时进行的,因为这个时候窗口的客户区和非客户区都已经被创建了,窗口还没有显示给用户,这恰恰是设置界面的机会。

除了标题为Close的按钮外,主窗口中各区域的图形都是在响应WM_PAINT消息时程序自己画上去的,相关代码如下。

void CMainWindow::OnPaint()
{
  CPaintDC dc(this);

screenshot

// 画窗口左上角的正方形
  DrawMouseInput(&dc);
  // 画标题
  DrawMsgHeader(&dc);
  // 画消息框。DrawEdge函数绘制指定矩形的边框
  ::DrawEdge(dc, &m_rcMsgBoxBorder, EDGE_SUNKEN, BF_RECT);
  DrawMsg(&dc);
}
void CMainWindow::DrawMouseInput(CDC* pDC)
{
  HBRUSH hBrush = ::CreateSolidBrush(::GetSysColor(COLOR_3DFACE));
  HBRUSH hOldBrush = (HBRUSH)pDC->SelectObject(hBrush);
  // 画矩形
  pDC->Rectangle(&m_rcMouseDown);
  pDC->SelectObject(hOldBrush);
  ::DeleteObject(hBrush);
}
void CMainWindow::DrawMsgHeader(CDC* pDC)
{
  char* sz1 = "Caption:";
  char* sz2 = "Class:";
  char* sz3 = "Handle:";
  char* sz4 = "Name:";

  ::SetBkColor(*pDC, ::GetSysColor (COLOR_3DFACE));
  
  pDC->TextOut(m_ptHeaderOrigin.x, m_ptHeaderOrigin.y, sz1, strlen(sz1));
  pDC->TextOut(m_ptHeaderOrigin.x, m_ptHeaderOrigin.y + m_cyLine*1, sz2, strlen(sz2));
  pDC->TextOut(m_ptHeaderOrigin.x, m_ptHeaderOrigin.y + m_cyLine*2, sz3, strlen(sz3));
  pDC->TextOut(m_ptHeaderOrigin.x, m_ptHeaderOrigin.y + m_cyLine*3, sz4, strlen(sz4));

}

OnPaint使用3个自定义函数绘制了整个窗口界面。最后一个DrawMsg负责在m_rcMsgBox指定的区域显示目标窗口信息,下一个小节再详细谈论。

如果创建主窗口失败,或者在主窗口销毁时,框架程序都会调用CWnd类的虚函数PostNc Destroy执行清理代码。CMainWindow类重载此函数以删除程序在CMyApp::InitInstance函数中创建的CMainWindow对象。

void CMainWindow::PostNcDestroy()
{
  delete this;
}

6.7.2 获取目标窗口的信息
查看窗口信息时,06WinLooker首先取得鼠标所在处的窗口的句柄;然后在此窗口的外框上画一个红色的矩形,取得窗口的一些信息并显示在m_rcMsgBox指定的区域;最后,在鼠标下面的窗口变化时,程序擦去前一个窗口的矩形外框,开始新一轮循环。

CWindowInfo类的作用是管理目标窗口,它负责06WinLooker程序中目标窗口的数据的更新和边框的绘制,定义和实现它的代码如下。

// ----------------------------------looker.h文件---------------------------------//
#define BUFFER_SIZE 256

class CWindowInfo
{
public:
  CWindowInfo();
  // 擦除矩形外框
  void EraseFrame();
  // 更新数据
  void GetInfo(HWND hWnd);
  // 绘制矩形外框
  void DrawFrame();
  
  HWND m_hWnd;
  char m_szWindowCaption[BUFFER_SIZE];
  char m_szWindowClass[BUFFER_SIZE];
  char m_szExeFile[MAX_PATH];
};
// ----------------------------------looker.cpp文件---------------------------------//
CWindowInfo::CWindowInfo()
{
  m_hWnd = NULL;
}
void CWindowInfo::GetInfo(HWND hWnd)
{
  // 取得句柄、标题、类名
  m_hWnd = hWnd;
  ::GetWindowText(m_hWnd, m_szWindowCaption, BUFFER_SIZE);
  ::GetClassName(m_hWnd, m_szWindowClass, BUFFER_SIZE);

  // 取得磁盘上.exe文件的名称
  m_szExeFile[0] = '\0';
  DWORD nPID;
  // 取得包含窗口的进程的ID号
  ::GetWindowThreadProcessId(m_hWnd, &nPID);
  // 给系统中的所有进程拍一个快照,查找ID号为nPID的进程的信息
  HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, nPID);
  if(hProcessSnap == INVALID_HANDLE_VALUE)
    return;
  // 开始查找
  BOOL bFind = FALSE;
  PROCESSENTRY32 pe32 = { sizeof(pe32) };
  if(::Process32First(hProcessSnap, &pe32))
  {
    do
    {
      if(pe32.th32ProcessID == nPID)
      {
        bFind = TRUE;
        break;
      }
    }while(::Process32Next(hProcessSnap, &pe32));
  }
  ::CloseHandle(hProcessSnap);
  // 只保存文件名结构中文件的名称(不包括目录)
  if(bFind)
  {
    const char* pszExeFile = strrchr(pe32.szExeFile, '\\');
    if(pszExeFile == NULL)
      pszExeFile = pe32.szExeFile;
    else
      pszExeFile++;
    strcpy(m_szExeFile, pszExeFile);
  }  
}
void CWindowInfo::DrawFrame()
{
  // 目标窗口的设备环境句柄
  HDC hdc = ::GetWindowDC(m_hWnd);
  // 目标窗口外框的大小
  RECT rcFrame;
  ::GetWindowRect(m_hWnd, &rcFrame);
  int nWidth = rcFrame.right - rcFrame.left;
  int nHeight = rcFrame.bottom - rcFrame.top;

  // 用红色笔沿外框四周画线
  HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(255,0,0));
  HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

  ::MoveToEx(hdc, 0, 0, NULL);
  ::LineTo(hdc, nWidth, 0);
  ::LineTo(hdc, nWidth, nHeight);
  ::LineTo(hdc, 0, nHeight);
  ::LineTo(hdc, 0, 0);

  ::SelectObject(hdc, hOldPen);
  ::DeleteObject(hPen);
  ::ReleaseDC(m_hWnd, hdc);
}
void CWindowInfo::EraseFrame()
{
  // 重画本窗口的非客户区部分(RDW_FRAME、RDW_INVALIDATE标记),
  // 立即更新(RDW_UPDATENOW标记)
  ::RedrawWindow(m_hWnd, NULL, NULL,
    RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW);

  HWND hWndParent = ::GetParent(m_hWnd);
  if(::IsWindow(hWndParent))
  {
    // 重画父窗口的整个客户区(RDW_ERASE、RDW_INVALIDATE标记),
    // 立即更新(RDW_UPDATENOW标记), 包括所有子窗口(RDW_ALLCHILDREN标记)
    ::RedrawWindow(hWndParent, NULL, NULL, 
      RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
  } 
}

GetInfo函数负责更新CWindowInfo对象中的数据,它通过一组ToolHelp函数取得进程内主模块的镜像文件名(可执行文件的名称),这组函数声明在tlhelp32.h头文件中,所以在程序的开头应包含此文件。擦除目标窗口的红色外框就是使目标窗口和其父窗口(如果有的话)重画一次,Windows提供的函数RedrawWindow可以用来重画窗口,原型如下。

BOOL RedrawWindow(
 HWND hWnd,         // 窗口句柄
 CONST RECT *lprcUpdate,  // 要更新的矩形的位置坐标。如果hrgnUpdate指定了一个区域,此参数会被忽略
 HRGN hrgnUpdate,     // 要更新的区域的句柄。值为NULL时,整个客户区会被添加到待更新的区域中
 UINT flags        // 重画标志
);

现在在CMainWindow类中添加一个CWindowInfo类型的成员变量m_wndInfo,此成员代表当前正在操作的目标窗口。

CWindowInfo m_wndInfo;    // 一个目标窗口对象
CMainWindow类的成员函数DrawMsg将显示出目标窗口对象中的数据。

void CMainWindow::DrawMsg(CDC* pDC)
{
  if(m_wndInfo.m_hWnd == NULL)
    return;  
  int xPos = m_rcMsgBox.left;
  int yPos = m_rcMsgBox.top;
  char sz[32];
  wsprintf(sz, "0X%0X", (int)m_wndInfo.m_hWnd);

  ::SetBkColor(*pDC, ::GetSysColor(COLOR_3DFACE));

  pDC->TextOut(xPos, yPos, 
    m_wndInfo.m_szWindowCaption, strlen(m_wndInfo.m_szWindowCaption));
  pDC->TextOut(xPos, yPos + m_cyLine*1, 
    m_wndInfo.m_szWindowClass, strlen(m_wndInfo.m_szWindowClass));
  pDC->TextOut(xPos, yPos + m_cyLine*2, 
    sz, strlen(sz));
  pDC->TextOut(xPos, yPos + m_cyLine*3, 
    m_wndInfo.m_szExeFile, strlen(m_wndInfo.m_szExeFile));
}

06WinLooker的主要功能是在响应鼠标事件时完成的,下面是相关的程序代码。

void CMainWindow::OnLButtonDown(UINT nFlags, CPoint point)
{
  // PtInRect函数用于判断point的位置是否在m_rcMouseDown指定的矩形区域中
  if(!m_bCatchMouseDown && ::PtInRect(&m_rcMouseDown, point))
  {
    // 在的话就更换光标形状,捕获鼠标输入,设置标志信息
    m_wndInfo.m_hWnd = NULL;
    ::SetCursor(m_hCursorTarget);
    ::SetCapture(m_hWnd);
    m_bCatchMouseDown = TRUE;
  }
}
void CMainWindow::OnLButtonUp(UINT nFlags, CPoint point)
{
  if(m_bCatchMouseDown)
  {
    // 恢复光标状态,释放捕获的鼠标输入,擦除目标窗口的矩形框架
    ::SetCursor(m_hCursorArrow);
    ::ReleaseCapture();
    m_bCatchMouseDown = FALSE;
    if(m_wndInfo.m_hWnd != NULL)
      m_wndInfo.EraseFrame();
  }
}
void CMainWindow::OnMouseMove(UINT nFlags, CPoint point)
{
  if(m_bCatchMouseDown)
  {
    // 将客户区坐标转换为屏幕坐标
    ::ClientToScreen(m_hWnd, &point);
    // 取得鼠标所在处的窗口的句柄
    HWND hWnd = ::WindowFromPoint(point);
    if(hWnd == m_wndInfo.m_hWnd)
      return;

    // 擦除前一个窗口上的红色框架,取得新的目标窗口的信息,绘制框架
    m_wndInfo.EraseFrame();
    m_wndInfo.GetInfo(hWnd);
    m_wndInfo.DrawFrame();

    // 通过无效显示区域,使窗口客户区重画
    ::InvalidateRect(m_hWnd, &m_rcMsgBox, TRUE);
  }
}

在同一时间仅能有一个窗口获得鼠标输入。SetCapture函数可以使当前线程中指定的窗口捕获鼠标输入,这样,当在此窗口上按下鼠标左建,再拖动鼠标到其他窗口时,系统还会将鼠标产生的消息发送到此窗口,而不使其他窗口获得输入焦点。当窗口不需要接收所有的鼠标输入时,创建此窗口的线程应该调用ReleaseCapture函数释放鼠标输入。

现在运行应用程序,除了没有“Close”按钮外,所有的功能都实现了。

6.7.3 自制按钮
按钮是子窗口。在Windows下每个子窗口都有一个ID号,当有消息产生时,它会向父窗口发送WM_COMMAND消息,消息的wParam参数高字位包含了通知代码,低字位包含了发送此消息的子窗口ID号,lParam参数的值是子窗口句柄。例如,当用鼠标左键单击按钮时,此按钮向其父窗口发送的WM_COMMAND消息中,wParam参数的高字位为BN_CLICKED,低字位为这个按钮的ID号。下面是子窗口向其父窗口发送鼠标单击事件的代码。

::SendMessage(::GetParent(m_hWnd), WM_COMMAND, 
      MAKEWPARAM(::GetDlgCtrlID(m_hWnd), BN_CLICKED), (LPARAM)m_hWnd);

GetDlgCtrlID函数通过子窗口句柄得到它的ID号,这个ID号是在创建子窗口时作为菜单句柄传给CreateWindowEx函数的值。MAKEWPARAM宏以第一个参数为低16位,第二个参数为高16位创建一个新值。

在封装自己的按钮类时,还有一点需要注意:只有当用户在按钮上按下鼠标左键,然后还是在这个窗口上释放左键时,按钮才应该向父窗口发送通知消息;如果用户在按钮上按下了左键,而在别的窗口上释放,那么这个按钮就不应该发送通知消息。

下面是CMyButton类的源代码,分别放在MyButton.h和MyButton.cpp两个文件中。

// -------------------------------------------------------MyButton.h文件----------------------------------------------------//
#ifndef __MYBUTTON_H__
#define __MYBUTTON_H__
#include "afxwin.h"

class CMyButton : public CWnd 
{
public:
  CMyButton(LPCTSTR lpszText, const RECT& rect, CWnd* pParentWnd, UINT nID);
protected:
  char m_szText[256];    // 按钮显示的文本
  BOOL m_bIsDown;    // 指示用户是否按下鼠标左键

  virtual void PostNcDestroy();
  afx_msg void OnPaint();
  afx_msg void OnLButtonDown(UINT nFlags, POINT point);
  afx_msg void OnLButtonUp(UINT nFlags, POINT point);
  afx_msg void OnMouseMove(UINT nFlags, POINT point);

  DECLARE_MESSAGE_MAP()
};

#endif // __MYBUTTON_H__

// -----------------------------------------------MyButton.cpp文件------------------------------------------------------//
#include "MyButton.h"

BEGIN_MESSAGE_MAP(CMyButton, CWnd)
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()

CMyButton::CMyButton(LPCTSTR lpszText, const RECT& rect, CWnd* pParentWnd, UINT nID)
{
  m_bIsDown = FALSE;
  strncpy(m_szText, lpszText, 256);
  
  LPCTSTR pszClassName = AfxRegisterWndClass(0, 0, 
    (HBRUSH)(COLOR_BTNFACE + 1), AfxGetApp()->LoadStandardCursor(IDC_ARROW));

  Create(pszClassName, NULL, WS_CHILD|WS_VISIBLE, rect, pParentWnd, nID);
}
void CMyButton::OnPaint()
{
  CPaintDC dc(this);
  ::SetBkMode(dc, TRANSPARENT);

  // 创建字体
  HFONT hFont = ::CreateFont(12, 0, 0, 0, FW_HEAVY, 0, 0, 0, ANSI_CHARSET, 
        OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, 
        VARIABLE_PITCH | FF_SWISS, "MS Sans Serif" );
  HFONT hOldFont = (HFONT)SelectObject(dc, hFont);
  // 创建画刷和画笔
  HBRUSH hBrush, hOldBrush;
  HPEN hPen, hOldPen;
  if(m_bIsDown)
  {
    hBrush = ::CreateSolidBrush(RGB(0xa0, 0xa0, 0xa0));
    hPen = ::CreatePen(PS_SOLID, 1, RGB(0x64, 0x64, 0x64));
    ::SetTextColor(dc, RGB(0x32, 0x32, 0xfa));
  }
  else
  {  
    hBrush = ::CreateSolidBrush(RGB(0xf0, 0xf0, 0xf0));
    hPen = ::CreatePen(PS_SOLID, 1, RGB(0x78, 0x78, 0x78));
    ::SetTextColor(dc, RGB(0x32, 0x32, 0x32));  
  }
  hOldBrush = (HBRUSH)::SelectObject(dc, hBrush);
  hOldPen = (HPEN)::SelectObject(dc, hPen);
  
  // 绘制外框和文本
  RECT rcClient;
  ::GetClientRect(m_hWnd, &rcClient);
  ::RoundRect(dc, rcClient.left, rcClient.top, rcClient.right, rcClient.bottom, 2, 2);
  ::DrawText(dc, m_szText, strlen(m_szText), &rcClient, DT_CENTER|DT_SINGLELINE|DT_VCENTER);

  // 清除资源
  ::DeleteObject(::SelectObject(dc, hOldFont));
  ::DeleteObject(::SelectObject(dc, hOldPen));
  ::DeleteObject(::SelectObject(dc, hOldBrush));  
}
void CMyButton::OnLButtonDown(UINT nFlags, POINT point)
{
  m_bIsDown = TRUE;
  ::InvalidateRect(m_hWnd, NULL, TRUE);
}
void CMyButton::OnLButtonUp(UINT nFlags, POINT point)
{
  if(m_bIsDown)
  {
    ::InvalidateRect(m_hWnd, NULL, TRUE);
    ::SendMessage(::GetParent(m_hWnd), WM_COMMAND, 
      MAKEWPARAM(::GetDlgCtrlID(m_hWnd), BN_CLICKED), (LPARAM)m_hWnd);
    m_bIsDown = FALSE;
  }
}
void CMyButton::OnMouseMove(UINT nFlags, POINT point)
{
  RECT rc;
  ::GetClientRect(m_hWnd, &rc);
  if(::PtInRect(&rc, point))
  {
    ::SetCapture(m_hWnd);
  }
  else
  {
    ::InvalidateRect(m_hWnd, NULL, TRUE);
    ::ReleaseCapture();
    m_bIsDown = FALSE;
  }    
}
void CMyButton::PostNcDestroy()
{
  delete this;
}

当用户鼠标移动到按钮窗口时,CMyButton类立即调用SetCapture函数捕获鼠标输入,这样就可以知道用户鼠标何时离开了按钮窗口,以便设置m_bIsDown的值为FALSE。

使用CMyButton类创建按钮控件很简单,只要用合适的参数创建一个此类的对象即可。06WinLooker程序在主窗口接受到WM_CREATE消息时创建Close按钮,所以应在CMainWindow::OnCreate函数中添加如下代码。

RECT rcButton = {12, m_rcMsgBoxBorder.bottom - 18, 64, m_rcMsgBoxBorder.bottom };
new CMyButton("Close", rcButton, this, IDB_CLOSE);

为了响应CMyButton按钮发来的消息,应在CMainWindow类中重载CWnd类的虚函数OnCommand,代码如下所示。

BOOL CMainWindow::OnCommand(WPARAM wParam, LPARAM lParam)
{
  if(LOWORD(wParam) == IDB_CLOSE)
  {
    DestroyWindow();
    return TRUE;  // 返回TRUE说明此消息已经处理,阻止CWnd类继续处理
  }
  return FALSE;
}

运行程序,单击Close按钮,看看效果吧。这个CMyButton类是Windows标准按钮控件的一个缩影。

相关文章
|
9天前
|
消息中间件 编译器 API
Windows窗口程序
Windows窗口程序
|
3月前
|
API Python Windows
python3应用windows api对后台程序窗口及桌面截图并保存的方法
python3应用windows api对后台程序窗口及桌面截图并保存的方法
89 1
|
1月前
|
弹性计算 安全 数据安全/隐私保护
远程桌面连接Windows实例,提示“为安全考虑,已锁定该用户账户,原因是登录尝试或密码更改尝试过多”错误解决方案
远程桌面连接Windows实例,提示“为安全考虑,已锁定该用户账户,原因是登录尝试或密码更改尝试过多”错误解决方案
158 0
|
2月前
|
弹性计算 编解码 监控
ECS实例问题之ECS实例无法选择Windows操作系统如何解决
ECS实例指的是在阿里云ECS服务中创建的虚拟计算环境,用户可在此环境中运行应用程序和服务;本合集将介绍ECS实例的创建、管理、监控和维护流程,及常见问题处理方法,助力用户保障实例的稳定运行。
|
3月前
|
弹性计算 Windows
Windows系统的ECS实例如何配置RAID10
Windows系统的ECS实例如何配置RAID10
|
4月前
|
Windows
(查看,和保存)windows下通过cmd命令符窗口查看、保存文件目录结构
(查看,和保存)windows下通过cmd命令符窗口查看、保存文件目录结构
59 0
|
4月前
|
XML Arthas Java
Windows【工具 04】WinSW官网使用说明及实例分享(将exe和jar注册成服务)实现服务器重启后的服务自动重启
Windows【工具 04】WinSW官网使用说明及实例分享(将exe和jar注册成服务)实现服务器重启后的服务自动重启
105 0
|
7月前
|
存储 固态存储 Windows
在 Windows 中,当一个应用程序窗口被关闭,该应用程序将会保留在哪里?
在 Windows 中,当一个应用程序窗口被关闭,该应用程序将会保留在哪里?
89 0
|
7月前
|
编解码 开发工具 Android开发
Windows平台RTMP多实例推送探讨
之前,我们博客 https://blog.csdn.net/renhui1112/article/details/105624392 提到,Android平台RTMP多实例推送的几种情况探讨,简单来说有以下三种情况: 1. 多路编码,多个实例分别推送到不同的RTMP URL(如Android采集板卡同时接2路出去); 2. 同一路编码,多个实例分别推送到不同的RTMP URL(如推送到内网、外网不同的RTMP服务器); 3. 部分路编码、部分路对接编码后的H.264/AAC数据,多个实例分别推送到不同的RTMP URL(混合推)。
|
8月前
|
API C++ Windows
Windows入门篇一之MSDN手册的使用和第一个窗口程序
Windows入门篇一之MSDN手册的使用和第一个窗口程序
Windows入门篇一之MSDN手册的使用和第一个窗口程序