[Win32/WTL]_[初级]_[如何销毁自定义控件]
场景
- 在开发
Win32/WTL
程序时,有时候需要对创建的控件进行删除再重建。比如用某些按钮表示系统里的磁盘,当插入拔出U
盘时,需要删除对应按钮,或新建新的按钮表示插入的U
盘。实现这个按钮我们用的是CWindowImpl
的子类,有什么要注意的地方吗?
说明
Win32
的窗口是对应着一个窗口句柄类型HWND
。在atlwin.h
里的类CWindowImpl
当然也会持有一个句柄成员变量进行维护。所以我们销毁一个自定义窗口类时,当然也需要销毁这个HWND
句柄。销毁句柄调用winuser.h
的函数DestroyWindow
[1].
BOOL DestroyWindow(
[in] HWND hWnd
);
-
这个函数只是销毁
HWND
对象,并不会对CWindow
成员变量m_hWnd
赋值为NULL
。这个CWindow
也是整个继承树的最终父类TBase
。 -
我们看看在
CWindowImpl
子类调用成员方法DestroyWindow
时,实际上调用的是它的父类CWindowImplRoot<TBase>
的DestroyWindow
方法, 它里面实际调用的是全局的::DestroyWindow
函数。 可以对这个全局的函数调用时打断点,可以发现它调用结束后m_hWnd
的值是NULL
。为什么呢?
BOOL DestroyWindow()
{
#ifndef ATL_NO_ASSERT_ON_DESTROY_NONEXISTENT_WINDOW
ATLASSERT(::IsWindow(this->m_hWnd));
#endif
if (!::DestroyWindow(this->m_hWnd))
{
return FALSE;
}
return TRUE;
}
- 因为在调用
::DestroyWindow
时,会同步发送消息WM_DESTROY
和WM_NCDESTROY
。 。CWindowImplRoot<TBase>
的子类CWindowImplBaseT
注册了的消息处理函数里对这些消息进行了处理。并在CWindowImplBaseT< TBase, TWinTraits >::WindowProc
方法里赋值pThis->m_hWnd = NULL;
。因此上边第3点说的调用::DestroyWindow
返回后m_hWnd
为NULL
. 但是要注意,它的前提条件是满足if((pThis->m_dwState & CWindowImplRoot<TBase>::WINSTATE_DESTROYED) && pOldMsg== NULL)
.如果没有调用这行,我们在CWindowImpl
子类析构里是需要调用Detach()
方法。
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(
_In_ HWND hWnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam)
{
CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
ATLASSERT(pThis != NULL);
if(!pThis)
{
return 0;
}
pThis->m_hWnd = hWnd;
// Initialize the thunk. This is allocated in CWindowImplBaseT::Create,
// so failure is unexpected here.
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
#ifdef _DEBUG
// check if somebody has subclassed us already since we discard it
if(pOldProc != StartWindowProc)
ATLTRACE(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n"));
#else
(pOldProc); // avoid unused warning
#endif
return pProc(hWnd, uMsg, wParam, lParam);
}
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(
_In_ HWND hWnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam)
{
CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
// set a ptr to this message and save the old value
_ATL_MSG msg(pThis->m_hWnd, uMsg, wParam, lParam);
const _ATL_MSG* pOldMsg = pThis->m_pCurrentMsg;
pThis->m_pCurrentMsg = &msg;
// pass to the message map to process
LRESULT lRes = 0;
BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);
// restore saved value for the current message
ATLASSERT(pThis->m_pCurrentMsg == &msg);
// do the default processing if message was not handled
if(!bRet)
{
if(uMsg != WM_NCDESTROY)
lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
else
{
// unsubclass, if needed
LONG_PTR pfnWndProc = ::GetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC);
lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC) == pfnWndProc)
::SetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC, (LONG_PTR)pThis->m_pfnSuperWindowProc);
// mark window as destroyed
pThis->m_dwState |= CWindowImplRoot<TBase>::WINSTATE_DESTROYED;
}
}
if((pThis->m_dwState & CWindowImplRoot<TBase>::WINSTATE_DESTROYED) && pOldMsg== NULL)
{
// clear out window handle
HWND hWndThis = pThis->m_hWnd;
pThis->m_hWnd = NULL;
pThis->m_dwState &= ~CWindowImplRoot<TBase>::WINSTATE_DESTROYED;
// clean up after window is destroyed
pThis->m_pCurrentMsg = pOldMsg;
pThis->OnFinalMessage(hWndThis);
}else {
pThis->m_pCurrentMsg = pOldMsg;
}
return lRes;
}
- 注意,如果不在
CWindowImpl
子类的析构函数里调用DestroyWindow()
方法,在父类的析构函数~CWindowImplRoot()
里会进行m_hWnd
成员变量的断言失败。
virtual ~CWindowImplRoot()
{
#ifdef _DEBUG
if(this->m_hWnd != NULL) // should be cleared in WindowProc
{
ATLTRACE(atlTraceWindowing, 0, _T("ERROR - Object deleted before window was destroyed\n"));
ATLASSERT(FALSE);
}
#endif //_DEBUG
}
- 完整的销毁子控件方法.
BASSwitchButton::~BASSwitchButton()
{
if (m_hWnd) {
DestroyWindow();
//Detach(); // 不需要
// delete 图片或其他
}
}
例子
- 这里写了一个例子是动态创建和销毁按钮的例子。
bas_switch_button.h
#pragma once
#include <Windows.h>
#include <atlbase.h>
#include <atlapp.h>
#include <atlcrack.h>
#include <atlwin.h>
#include "atlframe.h"
#include <atlctrls.h>
#include <atlctrlx.h>
#include <atlmisc.h>
#include <GdiPlus.h>
#include <string>
class BASSwitchButton:public CWindowImpl<BASSwitchButton,CButton>
{
public:
BEGIN_MSG_MAP_EX(BASSwitchButton)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MSG_WM_ERASEBKGND(OnEraseBkgnd)
REFLECTED_NOTIFY_CODE_HANDLER(BCN_HOTITEMCHANGE,OnHotItemChange2)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
BASSwitchButton();
~BASSwitchButton();
void SetInitVisible(bool visible = true);
HWND CreateButton(HWND hWndParent,_U_MENUorID MenuOrID = 0U);
inline BASSwitchButton& SetRadio(bool radio)
{
is_radio_button_ = radio;
return *this;
}
inline BASSwitchButton& SetHMargin(int hmargin)
{
hmargin_ = hmargin;
return *this;
}
inline BASSwitchButton& SetVMargin(int vmargin)
{
vmargin_ = vmargin;
return *this;
}
inline BASSwitchButton& SetColorDisable(DWORD disable_bkg_color,DWORD disable_font_color)
{
disable_bkg_color_ = disable_bkg_color;
disable_font_color_ = disable_font_color;
return *this;
}
inline BASSwitchButton& SetBackgroundColor(DWORD color)
{
color_bg_ = color;
return *this;
}
inline BASSwitchButton& SetRoundRectPoint(CPoint point)
{
round_rect_point_ = point;
return *this;
}
inline BASSwitchButton& SetRoundRect(bool is_round_rect)
{
is_round_rect_ = is_round_rect;
return *this;
}
inline BASSwitchButton& SetColorBorder(DWORD color_border)
{
color_border_ = color_border;
return *this;
}
BOOL Init(int nPosx,int nPosy,LPCWSTR text,
COLORREF normal_bkg_color,COLORREF normal_font_color,
COLORREF pressed_bkg_color,COLORREF pressed_font_color);
void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void DrawRect(Gdiplus::Graphics& graphics,Gdiplus::Rect rect,DWORD color);
void ResetStatus();
void SetPressedStatus();
BOOL EnableWindow(_In_ BOOL bEnable = TRUE);
void SetFont(Gdiplus::Font* font,bool deleteOld = true);
protected:
LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnHotItemChange2(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
BOOL OnEraseBkgnd(CDCHandle dc);
private:
inline void SetTextColor(COLORREF color){
color_text_ = color;
}
void SetTextString(LPCWSTR text);
int nPosx_;
int nPosy_;
bool is_radio_button_;
Gdiplus::Font *font_;
COLORREF color_text_;
std::wstring text_;
DWORD normal_bkg_color_;
DWORD normal_font_color_;
DWORD pressed_bkg_color_;
DWORD pressed_font_color_;
DWORD disable_bkg_color_;
DWORD disable_font_color_;
int hmargin_;
int vmargin_;
int button_width_;
int button_height_;
int status_;
bool m_bTracking_;
bool pressed_;
bool is_round_rect_;
CPoint round_rect_point_;
COLORREF color_border_;
COLORREF color_bg_;
int scaling_; // 缩放系数
bool visible_; // 是否创建可见窗口.
};
bas_switch_button.cpp
#include "stdafx.h"
#include "bas_switch_button.h"
#include <assert.h>
#include <iostream>
#include <sstream>
#include "utils.h"
namespace
{
enum
{
kButtonStatusNormal,
kButtonStatusPressed,
kButtonStatusHover,
kButtonStatusDisable
};
}
void BASSwitchButton::SetInitVisible(bool visible)
{
visible_ = visible;
}
BOOL BASSwitchButton::EnableWindow(BOOL bEnable)
{
auto result = CWindowImpl::EnableWindow(bEnable);
status_ = (bEnable)?kButtonStatusNormal:kButtonStatusDisable;
// 1.需要再调用一次绘图事件绘制当前的状态。
Invalidate(FALSE);
UpdateWindow();
return result;
}
BASSwitchButton::BASSwitchButton()
{
disable_bkg_color_ = -1;
disable_font_color_ = -1;
color_bg_ = RGB(255,255,255);
visible_= true;
hmargin_ = 10;
vmargin_ = 10;
is_radio_button_ = true;
color_border_ = -1;
disable_bkg_color_ = RGB(127,127,127);
disable_font_color_ = RGB(255,255,255);
is_round_rect_ = false;
round_rect_point_.x = 6.0;
round_rect_point_.y = 6.0;
font_ = NULL;
}
void BASSwitchButton::SetFont(Gdiplus::Font* font,bool deleteOld)
{
if(deleteOld)
delete font_;
font_ = font;
}
BASSwitchButton::~BASSwitchButton()
{
if (m_hWnd) {
DestroyWindow();
//Detach();
}
}
void BASSwitchButton::SetPressedStatus()
{
status_ = kButtonStatusPressed;
Invalidate(FALSE);
}
void BASSwitchButton::ResetStatus()
{
status_ = kButtonStatusNormal;
Invalidate(FALSE);
}
void BASSwitchButton::DrawRect(Gdiplus::Graphics& graphics,Gdiplus::Rect rect,DWORD color)
{
Gdiplus::GraphicsPath m_pPath;
m_pPath.AddRectangle(rect);
m_pPath.CloseFigure();
Gdiplus::Color color_bg(GetRValue(color_bg_),GetGValue(color_bg_),
GetBValue(color_bg_));
Gdiplus::SolidBrush brush_bg(color_bg);
graphics.FillPath(&brush_bg,&m_pPath);
Gdiplus::Pen *pen = NULL;
Gdiplus::Color gcolor(GetRValue(color),GetGValue(color),GetBValue(color));
if(color_border_ == -1){
pen = new Gdiplus::Pen(gcolor,1);
}else{
Gdiplus::Color color_border(GetRValue(color_border_),GetGValue(color_border_),
GetBValue(color_border_));
pen = new Gdiplus::Pen(color_border,1);
}
Gdiplus::SolidBrush brush_color(gcolor);
Gdiplus::GraphicsPath path_border;
if(is_round_rect_){
//Utils::CreateRoundRect(path_border,rect,round_rect_point_.x);
}else{
path_border.AddRectangle(rect);
path_border.CloseFigure();
}
graphics.FillPath(&brush_color,&path_border);
if(color_border_ != -1)
graphics.DrawPath(pen,&path_border);
}
LRESULT BASSwitchButton::OnHotItemChange2(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
//OutputDebugString(L"OnHotItemChange \n");
NMBCHOTITEM* item = (NMBCHOTITEM*)pnmh;
if(item->dwFlags & HICF_ENTERING){
//OutputDebugString(L"HICF_ENTERING \n");
if (!is_radio_button_){
status_ = kButtonStatusHover;
}else if(status_ != kButtonStatusPressed){
status_ = kButtonStatusHover;
}
}else{
status_ = kButtonStatusNormal;
//OutputDebugString(L"HICF_LEAVING \n");
}
SetMsgHandled(FALSE);
return 0;
}
void BASSwitchButton::SetTextString(LPCWSTR text)
{
text_ = text;
Gdiplus::Graphics graphics(m_hWnd);
Gdiplus::RectF rect_text;
graphics.MeasureString(text_.c_str(),text_.size(),font_,
Gdiplus::PointF(0,0),&rect_text);
button_width_ = rect_text.Width+hmargin_*2;
button_height_ = rect_text.Height + vmargin_*2;
}
LRESULT BASSwitchButton::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
// Ellipse
CPaintDC hdc(m_hWnd);
CRect rect;
GetClientRect(&rect);
Gdiplus::Bitmap bmp(int(rect.Width()),int(rect.Height()));
Gdiplus::Graphics graphics(&bmp);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintSystemDefault);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
// 按钮窗口创建可能有1像素的边框区域,需要夸扩大左上区域1像素才可以把边界绘制到.
// 不清楚原因.
rect.InflateRect(1,1,0,0);
Gdiplus::Rect client_rect(rect.left,rect.top,rect.Width(),rect.Height());
switch(status_)
{
case kButtonStatusNormal:
{
SetTextColor(normal_font_color_);
DrawRect(graphics,client_rect,normal_bkg_color_);
break;
}
case kButtonStatusPressed:
{
SetTextColor(pressed_font_color_);
DrawRect(graphics,client_rect,pressed_bkg_color_);
break;
}
case kButtonStatusHover:
{
SetTextColor(pressed_font_color_);
DrawRect(graphics,client_rect,pressed_bkg_color_);
break;
}
case kButtonStatusDisable:
{
SetTextColor(disable_font_color_);
DrawRect(graphics,client_rect,disable_bkg_color_);
break;
}
}
if(text_.size()){
Gdiplus::RectF rect(client_rect.X,client_rect.Y,client_rect.Width,client_rect.Height);
Gdiplus::Color color_text;
color_text.SetFromCOLORREF(color_text_);
Utils::DrawTextCenter(graphics,rect,text_.c_str(),font_,&color_text);
}
///*Drawing on DC*/
Gdiplus::Graphics graphics1(hdc);
///*Important! Create a CacheBitmap object for quick drawing*/
Gdiplus::CachedBitmap cachedBmp(&bmp,&graphics1);
graphics1.DrawCachedBitmap(&cachedBmp,0,0);
bHandled = TRUE;
return 0;
}
HWND BASSwitchButton::CreateButton(HWND hWndParent,_U_MENUorID MenuOrID)
{
m_bTracking_ = false;
status_ = kButtonStatusNormal;
// BS_NOTIFY WS_CLIPCHILDREN
int flag = WS_CHILD | WS_CLIPCHILDREN ;
if(visible_)
flag |= WS_VISIBLE;
return Create(hWndParent,NULL,L"",flag,0,MenuOrID);
}
BOOL BASSwitchButton::OnEraseBkgnd(CDCHandle dc)
{
return TRUE;
}
LRESULT BASSwitchButton::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
Gdiplus::FontFamily fontFamily(L"Arial");
font_ = new Gdiplus::Font(&fontFamily,16,
(false)?Gdiplus::FontStyleBold:Gdiplus::FontStyleRegular,Gdiplus::UnitPixel);
color_text_ = RGB(0,0,0);
bHandled = FALSE;
return 0;
}
BOOL BASSwitchButton::Init(int nPosx,int nPosy,LPCWSTR text,
COLORREF normal_bkg_color,COLORREF normal_font_color,
COLORREF pressed_bkg_color,COLORREF pressed_font_color)
{
normal_bkg_color_ = normal_bkg_color;
normal_font_color_ = normal_font_color;
pressed_bkg_color_ = pressed_bkg_color;
pressed_font_color_ = pressed_font_color;
nPosx_ = nPosx;
nPosy_ = nPosy;
SetTextString(text);
HWND wndparent=::GetParent(m_hWnd);
RECT rcClient;
::GetClientRect(wndparent,&rcClient);
if (nPosx<0)
nPosx=rcClient.right+nPosx-button_width_;
if (nPosy<0)
nPosy=rcClient.bottom+nPosy-button_height_;
SetWindowPos(NULL,nPosx,nPosy,button_width_,button_height_,SWP_FRAMECHANGED);
return 0;
}
View.h
// View.h : interface of the CView class
//
/
#ifndef VIEW_H
#define VIEW_H
#include <vector>
class BASSwitchButton;
class BASCOwnerDrawButton;
namespace Gdiplus{
class Font;
};
typedef enum ViewWindowId1 {
kViewWindowButtonMinId = WM_USER + 1000,
kViewWindowButtonMaxId = kViewWindowButtonMinId + 5000,
kViewWindowButtonAddButtonId,
kViewWindowButtonRemoveButtonId
}ViewWindowId;
class CView : public CWindowImpl<CView>
{
public:
DECLARE_WND_CLASS(NULL)
~CView();
BOOL PreTranslateMessage(MSG* pMsg);
BEGIN_MSG_MAP_EX(CView)
MSG_WM_CREATE(onCreate)
COMMAND_ID_HANDLER(kViewWindowButtonAddButtonId,onAddButton)
COMMAND_ID_HANDLER(kViewWindowButtonRemoveButtonId,onDeleteButton)
MESSAGE_HANDLER(WM_PAINT, onPaint)
MESSAGE_HANDLER(WM_CTLCOLORSTATIC, OnCtlColor)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
// Handler prototypes (uncomment arguments if needed):
// LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
// LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
// LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
protected:
LRESULT OnCtlColor(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT onPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
int onCreate(LPCREATESTRUCT lpCreateStruct);
LRESULT onAddButton(WORD wNotify,WORD wID,HWND hCtrl, BOOL &bHandeld);
LRESULT onDeleteButton(WORD wNotify,WORD wID,HWND hCtrl, BOOL &bHandeld);
void updateLayout();
private:
Gdiplus::Font *font_16_normal_gdi_;
CButton buttonAdd_;
CButton buttonRemove_;
std::vector<BASSwitchButton*> testButtons_;
int buttonId_ = kViewWindowButtonMinId;
};
#endif
View.cpp
// View.cpp : implementation of the CView class
//
/
#include "stdafx.h"
#include "resource.h"
#include <GdiPlus.h>
#include "View.h"
#include "base/bas_switch_button.h"
#include <functional>
CView::~CView()
{
}
BOOL CView::PreTranslateMessage(MSG* pMsg)
{
pMsg;
return FALSE;
}
LRESULT CView::OnCtlColor(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
HWND hWnd = (HWND)lParam;
HDC hdc = (HDC)wParam;
bHandled = FALSE;
return 0;
}
int CView::onCreate(LPCREATESTRUCT lpCreateStruct)
{
auto color_normal = RGB(13,164,230);
buttonAdd_.Create(m_hWnd,CRect(100,100,240,160),L"Add Button",
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,0,kViewWindowButtonAddButtonId);
buttonRemove_.Create(m_hWnd,CRect(300,100,440,160),L"Remove Button",
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,0,kViewWindowButtonRemoveButtonId);
return 0;
}
LRESULT CView::onPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
CPaintDC dc(m_hWnd);
//TODO: Add your drawing code here
return 0;
}
LRESULT CView::onAddButton(WORD wNotify,WORD wID,HWND hCtrl, BOOL &bHandeld)
{
auto _id = buttonId_++;
auto color_normal = RGB(13,164,230);
auto button = new BASSwitchButton();
button->SetInitVisible(true);
button->CreateButton(m_hWnd,_id);
button->SetHMargin(30)
.SetVMargin(8)
.SetRadio(false)
.SetColorBorder(color_normal)
.SetBackgroundColor(RGB(255,255,255));
std::wstring name(L"Button_");
name.append(std::to_wstring(_id));
button->Init(0,0,name.c_str(),color_normal,RGB(255,255,255),
RGB(234,245,249),RGB(0,0,0));
testButtons_.push_back(button);
updateLayout();
return 0;
}
LRESULT CView::onDeleteButton(WORD wNotify,WORD wID,HWND hCtrl, BOOL &bHandeld)
{
if (testButtons_.empty())
return 0;
auto last = testButtons_.back();
testButtons_.pop_back();
delete last;
updateLayout();
return 0;
}
void CView::updateLayout()
{
CRect rect;
GetClientRect(&rect);
static const int marginx = 100;
static const int marginy = 300;
static const int gap = 20;
int buttonx = marginx;
int buttony = marginy;
int maxRight = rect.right - marginx;
CRect rectButton;
for (auto i = 0; i < testButtons_.size(); ++i) {
auto one = testButtons_[i];
::GetClientRect(*one, &rectButton);
if ((buttonx + rectButton.Width()) > maxRight) {
buttonx = marginx;
buttony = rectButton.Height() + buttony + gap;
}
rectButton.MoveToXY(buttonx, buttony);
one->MoveWindow(rectButton, FALSE);
buttonx += (rectButton.Width() + gap);
}
}
运行图示
参考
- DestroyWindow 函数winuser.h