如何在 wxWidgets 中创建与 Window 的 (OS) 控制按钮完全一样的控制按钮?
How to create control buttons exactly like Window's (OS) control buttons in wxWidgets?
我想像 Windows 一样创建控制按钮(最小化、最大化和关闭)。
最终目标是创建类似 Microsoft Word 标题栏的东西。
我知道如何创建一个 wxButton,我也知道如何为它设置一个图标。但是我不知道如何使用原生 OS 图标或主题。
wxButton* closeButton = new wxButton(this, wxID_ANY, "x"); // how to tell that be like OS close button!
在 WinAPI 中,有一个名为 DrawThemeBackground 的函数,我可以将其与 WP_CLOSEBUTTON
一起使用,但我不知道 wxWidgets 中的等价物是什么。
更新:在大家的帮助下,这是在Windows中绘制本机按钮的示例代码(在其他OS中不起作用)。不幸的是,结果不是我想要的。它看起来像 Win XP 图标。 wxNativeRenderer 似乎不能正常工作。有人知道修复此代码吗? (是的,我添加了“wx.rc”资源文件并且我没有使用任何清单)
// wxWidgets "Hello World" Program
// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#include <wx/renderer.h>
#include <wx/artprov.h>
class MyApp: public wxApp
{
public:
virtual bool OnInit();
};
class MyFrame: public wxFrame
{
public:
MyFrame();
private:
};
wxIMPLEMENT_APP( MyApp );
bool MyApp::OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show( true );
return true;
}
wxBitmap getButtonBitmap( wxWindow* win, wxTitleBarButton type, const wxColour& bg, int flags = 0 )
{
const wxSize sizeBmp = wxArtProvider::GetSizeHint( wxART_BUTTON );
wxBitmap bmp( sizeBmp );
wxMemoryDC dc( bmp );
dc.SetBackground( bg );
dc.Clear();
wxRendererNative::Get().DrawTitleBarBitmap( win, dc, sizeBmp, type, flags );
return bmp;
}
MyFrame::MyFrame()
: wxFrame( NULL, wxID_ANY, "Hello World" )
{
wxWindow* win = this;
wxColour color = win->GetBackgroundColour();
// minimize button
wxBitmapButton* minimizeButton = new wxBitmapButton( win, wxID_ANY,
getButtonBitmap( win, wxTITLEBAR_BUTTON_ICONIZE, color ),
wxPoint( 0, 0 ), wxDefaultSize, wxBORDER_NONE );
minimizeButton->SetBitmapPressed( getButtonBitmap( win, wxTITLEBAR_BUTTON_ICONIZE, color, wxCONTROL_PRESSED ) );
minimizeButton->SetBitmapCurrent( getButtonBitmap( win, wxTITLEBAR_BUTTON_ICONIZE, color, wxCONTROL_CURRENT ) );
// maximize button
wxBitmapButton* maximizeButton = new wxBitmapButton( win, wxID_ANY,
getButtonBitmap( win, wxTITLEBAR_BUTTON_MAXIMIZE, color ),
wxPoint( 30, 0 ), wxDefaultSize, wxBORDER_NONE );
maximizeButton->SetBitmapPressed( getButtonBitmap( win, wxTITLEBAR_BUTTON_MAXIMIZE, color, wxCONTROL_PRESSED ) );
maximizeButton->SetBitmapCurrent( getButtonBitmap( win, wxTITLEBAR_BUTTON_MAXIMIZE, color, wxCONTROL_CURRENT ) );
// close Button
wxBitmapButton* closeButton = new wxBitmapButton( win, wxID_ANY,
getButtonBitmap( win, wxTITLEBAR_BUTTON_CLOSE, color ),
wxPoint( 60, 0 ), wxDefaultSize, wxBORDER_NONE );
closeButton->SetBitmapPressed( getButtonBitmap( win, wxTITLEBAR_BUTTON_CLOSE, color, wxCONTROL_PRESSED ) );
closeButton->SetBitmapCurrent( getButtonBitmap( win, wxTITLEBAR_BUTTON_CLOSE, color, wxCONTROL_CURRENT ) );
}
您应该能够使用这些按钮的原生外观绘制位图。然后在 wxButton 上使用这些位图。
有一些限制,但Windows应该对它们有最好的支持。见 wxRendererNative
::DrawTitleBarBitmap()
.
绘制按钮(如标题栏上的按钮)的过程比简单地使用 DrawThemeBackground
函数要复杂一些。这是一个演示,它部分展示了如何使用 wxWidgets 执行此操作:
#include "wx/wx.h"
#include <wx/dcclient.h>
#include <wx/mstream.h>
#include <wx/dcmemory.h>
#include <wx/rawbmp.h>
#include <wx/msw/wrapwin.h>
#include <uxtheme.h>
#include <Vssym32.h>
#include <map>
// Helper data types
struct BGInfo
{
wxRect BgRect;
wxRect SizingMargins;
wxRect ContentMargins;
int TotalStates;
};
struct ButtonInfo
{
wxRect ButtonRect;
int TotalStates;
};
enum class DPI
{
dpi96 = 0,
dpi120,
dpi144,
dpi196
};
enum class Button
{
Close = 0,
Min,
Max,
Restore,
Help
};
// Helper functions
void MarginsToRect(const MARGINS& m, wxRect& r)
{
r.SetLeft(m.cxLeftWidth);
r.SetRight(m.cxRightWidth);
r.SetTop(m.cyTopHeight);
r.SetBottom(m.cyBottomHeight);
}
void RectTowxRect(const RECT & r, wxRect& r2)
{
r2.SetLeft(r.left);
r2.SetTop(r.top);
r2.SetRight(r.right-1);
r2.SetBottom(r.bottom-1);
}
wxBitmap ExtractAtlas(const wxBitmap& atlas, int total, int loc)
{
int bgheight = atlas.GetHeight();
int individualHeight = bgheight/total;
int bgWidth = atlas.GetWidth();
int atlasOffset = individualHeight*loc;
wxRect bgRect = wxRect(wxPoint(0,atlasOffset),
wxSize(bgWidth,individualHeight));
return atlas.GetSubBitmap(bgRect);
}
void TileBitmap(const wxBitmap& bmp, wxDC& dc, const wxRect& r)
{
dc.SetClippingRegion(r);
for ( int y = 0 ; y < r.GetHeight() ; y += bmp.GetHeight() )
{
for ( int x = 0 ; x < r.GetWidth() ; x += bmp.GetWidth() )
{
dc.DrawBitmap(bmp, r.GetLeft() + x, r.GetTop() + y, true);
}
}
dc.DestroyClippingRegion();
}
void TileTo(const wxBitmap& in, const wxRect& margins, wxBitmap& out, int w, int h)
{
// Theoretically we're supposed to split the bitmap into 9 pieces based on
// the sizing margins and leave the 8 outside pieces as unchanged as
// possible and the fill the remainder with the center piece. However doing
// that doesn't look actual control buttons. So I'm going to just tile
// the center bitmap to fill the whole space.
int ml = margins.GetLeft();
int mr = margins.GetRight();
int mt = margins.GetTop();
int mb = margins.GetBottom();
int bw = in.GetWidth();
int bh = in.GetHeight();
wxBitmap center = in.GetSubBitmap(wxRect(wxPoint( ml,mt),wxSize(bw-ml-mr,bh-mb-mt)));
// Create and initially transparent bitmap.
unsigned char* data = reinterpret_cast<unsigned char*>(malloc(3*w*h));
unsigned char* alpha = reinterpret_cast<unsigned char*>(malloc(w*h));
memset(alpha, 0, w*h);
wxImage im(w, h, data, alpha);
wxBitmap bmp(im);
wxMemoryDC dc(bmp);
TileBitmap(center, dc, wxRect(wxPoint(0,0),wxSize(w,h)));
dc.SelectObject(wxNullBitmap);
out = bmp;
}
class MyFrame: public wxFrame
{
public:
MyFrame();
private:
void OnPaintImagePanel(wxPaintEvent&);
void OnListSelection(wxCommandEvent&);
void BuildItemToDraw();
void LoadThemeData();
wxListBox* m_typeBox, *m_dpiBox, *m_stateBox;
wxPanel* m_imagePanel;
wxBitmap m_fullAtlas;
wxBitmap m_itemToDraw;
BGInfo m_closeInfo;
BGInfo m_otherInfo;
std::map<std::pair<DPI,Button>,ButtonInfo> m_themeMap;
};
MyFrame::MyFrame():wxFrame(NULL, wxID_ANY, "Windows Control Button Demo", wxDefaultPosition,
wxSize(400, 300))
{
// Start all the image handlers. Only the PNG handler is actually needed.
::wxInitAllImageHandlers();
// Build the UI.
wxPanel* bg = new wxPanel(this, wxID_ANY);
wxStaticText* typeText = new wxStaticText(bg,wxID_ANY,"Type:");
m_typeBox = new wxListBox(bg,wxID_ANY);
wxStaticText* dpiText = new wxStaticText(bg,wxID_ANY,"dpi:");
m_dpiBox = new wxListBox(bg,wxID_ANY);
wxStaticText* stateText = new wxStaticText(bg,wxID_ANY,"State:");
m_stateBox = new wxListBox(bg,wxID_ANY);
m_imagePanel = new wxPanel(bg,wxID_ANY);
wxBoxSizer* mainSzr = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* boxSzr = new wxBoxSizer(wxHORIZONTAL);
boxSzr->Add(typeText, wxSizerFlags().Border(wxALL));
boxSzr->Add(m_typeBox, wxSizerFlags().Border(wxTOP|wxRIGHT|wxBOTTOM));
boxSzr->Add(dpiText, wxSizerFlags().Border(wxALL));
boxSzr->Add(m_dpiBox, wxSizerFlags().Border(wxTOP|wxRIGHT|wxBOTTOM));
boxSzr->Add(stateText, wxSizerFlags().Border(wxALL));
boxSzr->Add(m_stateBox, wxSizerFlags().Border(wxTOP|wxRIGHT|wxBOTTOM));
mainSzr->Add(boxSzr,wxSizerFlags());
mainSzr->Add(m_imagePanel,wxSizerFlags(1).Expand().Border(wxLEFT|wxRIGHT|wxBOTTOM));
bg->SetSizer(mainSzr);
// Set the needed event handlers for the controls.
m_imagePanel->Bind(wxEVT_PAINT, &MyFrame::OnPaintImagePanel, this);
m_typeBox->Bind(wxEVT_LISTBOX, &MyFrame::OnListSelection, this);
m_dpiBox->Bind(wxEVT_LISTBOX, &MyFrame::OnListSelection, this);
m_stateBox->Bind(wxEVT_LISTBOX, &MyFrame::OnListSelection, this);
// Concigure the controls.
m_typeBox->Append("Close");
m_typeBox->Append("Help");
m_typeBox->Append("Max");
m_typeBox->Append("Min");
m_typeBox->Append("Restore");
m_dpiBox->Append("96");
m_dpiBox->Append("120");
m_dpiBox->Append("144");
m_dpiBox->Append("192");
m_stateBox->Append("Normal");
m_stateBox->Append("Hot");
m_stateBox->Append("Pressed");
m_stateBox->Append("Inactive");
m_typeBox->Select(0);
m_dpiBox->Select(0);
m_stateBox->Select(0);
// Load the theme data and finish setting up.
LoadThemeData();
BuildItemToDraw();
}
void MyFrame::LoadThemeData()
{
HINSTANCE handle = LoadLibraryEx(L"C:\Windows\Resources\Themes\aero\aero.msstyles",
0, LOAD_LIBRARY_AS_DATAFILE);
if ( handle == NULL )
{
return;
}
HTHEME theme = OpenThemeData(reinterpret_cast<HWND>(this->GetHandle()),L"DWMWindow");
VOID* PBuf = NULL;
DWORD BufSize = 0;
GetThemeStream(theme, 0,0, TMT_DISKSTREAM, &PBuf, &BufSize, handle);
wxMemoryInputStream mis(PBuf,static_cast<int>(BufSize));
wxImage im(mis, wxBITMAP_TYPE_PNG);
if ( !im.IsOk() )
{
return;
}
wxBitmap b2(im);
m_fullAtlas = wxBitmap(im);;
MARGINS m;
RECT r;
int BUTTONACTIVECAPTION = 3;
int BUTTONACTIVECLOSE = 7;
int BUTTONCLOSEGLYPH96 = 11;
int BUTTONRESTOREGLYPH192 = 30;
// Store some of the theme info for the parts BUTTONACTIVECAPTION
// and BUTTONACTIVECLOSE.
GetThemeRect(theme, BUTTONACTIVECAPTION, 0, TMT_ATLASRECT, &r);
RectTowxRect(r,m_otherInfo.BgRect);
GetThemeMargins(theme,NULL, BUTTONACTIVECAPTION,0, TMT_CONTENTMARGINS,NULL, &m);
MarginsToRect(m,m_otherInfo.ContentMargins);
GetThemeMargins(theme,NULL, BUTTONACTIVECAPTION,0, TMT_SIZINGMARGINS,NULL, &m);
MarginsToRect(m,m_otherInfo.SizingMargins);
GetThemeInt(theme, BUTTONACTIVECAPTION, 0, TMT_IMAGECOUNT, &(m_otherInfo.TotalStates));
GetThemeRect(theme, BUTTONACTIVECLOSE, 0, TMT_ATLASRECT, &r);
RectTowxRect(r,m_closeInfo.BgRect);
GetThemeMargins(theme,NULL, BUTTONACTIVECLOSE,0, TMT_CONTENTMARGINS,NULL, &m);
MarginsToRect(m,m_closeInfo.ContentMargins);
GetThemeMargins(theme,NULL, BUTTONACTIVECLOSE,0, TMT_SIZINGMARGINS,NULL, &m);
MarginsToRect(m,m_closeInfo.SizingMargins);
GetThemeInt(theme, BUTTONACTIVECLOSE, 0, TMT_IMAGECOUNT, &(m_closeInfo.TotalStates));
// Since the part numbers for BUTTONCLOSEGLYPH96..BUTTONRESTOREGLYPH192
// are all sequential and the dpis all run from 96 to 192 in the same
// order, we can use a for loop to store
for ( int i = BUTTONCLOSEGLYPH96 ; i <= BUTTONRESTOREGLYPH192 ; ++i )
{
int j = i-BUTTONCLOSEGLYPH96;
Button b = static_cast<Button>(j/4);
DPI dpi = static_cast<DPI>(j%4);
std::pair<DPI,Button> item;
ButtonInfo info;
item = std::make_pair(dpi,b);
GetThemeRect(theme, i, 0, TMT_ATLASRECT, &r);
RectTowxRect(r,info.ButtonRect);
GetThemeInt(theme, i, 0, TMT_IMAGECOUNT, &(info.TotalStates));
m_themeMap.insert(std::make_pair(item,info));
}
CloseThemeData(theme);
FreeLibrary(handle);
}
void MyFrame::OnPaintImagePanel(wxPaintEvent&)
{
wxPaintDC dc(m_imagePanel);
dc.Clear();
if ( m_itemToDraw.IsOk() )
{
dc.DrawBitmap(m_itemToDraw,0,0,true);
}
}
void MyFrame::OnListSelection(wxCommandEvent&)
{
BuildItemToDraw();
}
void MyFrame::BuildItemToDraw()
{
BGInfo bginfo;
Button b = static_cast<Button>(m_typeBox->GetSelection());
DPI dpi = static_cast<DPI>(m_dpiBox->GetSelection());
int state = m_stateBox->GetSelection();
if ( b == Button::Close )
{
bginfo = m_closeInfo;
}
else
{
bginfo = m_otherInfo;
}
wxBitmap bgAtlas = m_fullAtlas.GetSubBitmap(bginfo.BgRect);
int totalbgs = bginfo.TotalStates;
wxBitmap bg = ExtractAtlas(bgAtlas, totalbgs, state);
std::pair<DPI,Button> item = std::make_pair(dpi,b);
auto it = m_themeMap.find(item);
if ( it != m_themeMap.end() )
{
ButtonInfo info = it->second;
wxBitmap itemAtlas = m_fullAtlas.GetSubBitmap(info.ButtonRect);
wxBitmap item = ExtractAtlas(itemAtlas, info.TotalStates, state);
wxRect contentmargins = bginfo.ContentMargins;
wxRect Sizingmargins = bginfo.SizingMargins;
int width = item.GetWidth() + contentmargins.GetLeft() + contentmargins.GetRight();
int height = item.GetHeight() + contentmargins.GetTop() + contentmargins.GetBottom();
if ( bg.GetWidth() > width )
{
width = bg.GetWidth();
}
if ( bg.GetHeight() > height )
{
height = bg.GetHeight();
}
wxBitmap bmp(width,height,32);
TileTo(bg,Sizingmargins, bmp, width, height);
wxMemoryDC dc(bmp);
int leftOffset = (width-item.GetWidth())/2;
int topOffset = (height - item.GetHeight())/2;
dc.DrawBitmap(item,leftOffset,topOffset, true);
dc.SelectObject(wxNullBitmap);
m_itemToDraw = bmp;
}
m_imagePanel->Refresh();
m_imagePanel->Update();
}
class MyApp : public wxApp
{
public:
virtual bool OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(MyApp);
这只是部分答案,因为,
- 这依赖于我只是输入代码的
BUTTONACTIVECAPTION
之类的数字部件号。这些数字最终是从文件 Aero.msstyles 中提取的,理论上,如果 Microsoft 更改该文件,代码中的数字可能是错误的。一个完整的答案将查看该文件并从中提取正确的数字,以便它始终可以确保它使用的是正确的数字。但是这样做超出了这个答案的范围。
- 我不确定如何获得按钮的大小。在我的系统上,关闭按钮的宽度为 45 像素,高度为 29 像素。但是我在任何主题数据中都看不到这些数字。
绘制这些按钮的技巧是您首先必须将主题文件作为 dll 打开。可以使用条目 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ThemeManager\DllName
从注册表中提取主题文件的名称。在上面的代码中,我只是将其硬编码为“C:\Windows\Resources\Themes\aero\aero.msstyles”,但从注册表中提取它可能比硬编码文件名更好。
打开主题后,特殊技巧是调用GetThemeStream函数。此 returns 内存中的 png 文件。它的第一部分如下所示:
如您所见,此 png 包含一堆控制按钮的图片。我们需要使用 GetThemeRect 函数来了解此 png 中与我们要绘制的部分对应的矩形。
但是现在我们运行遇到了问题。我们需要使用的主题class是“DWMWindow”。这个 class 完全没有文档,了解它的部分的唯一方法是使用像 msstyleEditor 这样的程序来查看主题文件。
从程序中可以看出我们感兴趣的零件号是:
int BUTTONACTIVECAPTION = 3;
int BUTTONACTIVECLOSE = 7;
int BUTTONCLOSEGLYPH96 = 11;
int BUTTONCLOSEGLYPH120 = 12;
int BUTTONCLOSEGLYPH144 = 13;
int BUTTONCLOSEGLYPH192 = 14;
int BUTTONHELPGLYPH96 = 15;
int BUTTONHELPGLYPH120 = 16;
int BUTTONHELPGLYPH144 = 17;
int BUTTONHELPGLYPH192 = 18;
int BUTTONMAXGLYPH96 = 19;
int BUTTONMAXGLYPH120 = 20;
int BUTTONMAXGLYPH144 = 21;
int BUTTONMAXGLYPH192 = 22;
int BUTTONMINGLYPH96 = 23;
int BUTTONMINGLYPH120 = 24;
int BUTTONMINGLYPH144 = 25;
int BUTTONMINGLYPH192 = 26;
int BUTTONRESTOREGLYPH96 = 27;
int BUTTONRESTOREGLYPH120 = 28;
int BUTTONRESTOREGLYPH144 = 29;
int BUTTONRESTOREGLYPH192 = 30;
有了这些部件号,我们可以使用 GetThemeRect
函数知道 png 的哪些部分用于我们要绘制的项目。
还有一些最后的问题。 GetThemeRect returns 为部分 BUTTONCLOSEGLYPH96 = 11
提供的矩形如下所示:
这称为图集,该子矩形中的 4 个部分分别对应于状态正常、热、推送和禁用。然而,由于 class 没有记录,唯一知道的方法是查看 msstyleEditor 的输出或从主题中获取它是其他方式。幸运的是,我们可以使用 GetThemeInt
和 TMT_IMAGECOUNT
属性 标识符来获取图集中的图像数量,这样至少我们知道要将其切成多少块。
我们可以从主题数据中提取更多信息。 GetThemeMargins
和 TMT_SIZINGMARGINS
属性 id 应该告诉我们如何将背景图像平铺成更大的尺寸。然而,在我的实验中,这些边缘的数字似乎并没有给出好的结果。因此,在上面的代码中,我只是平铺了中心部分来填充整个背景。此外,使用 TMT_CONTENTMARGINS
属性 id 应该可以告诉我们将字形放置在背景上的什么位置。但同样,在我的实验中,这些位置看起来不太好。所以在上面的代码中,我只是将字形置于背景的中心。
将所有这些放在一起,我们最终可以绘制关闭、最小值、最大值和恢复按钮,因为它们出现在标题栏上。
我想像 Windows 一样创建控制按钮(最小化、最大化和关闭)。
最终目标是创建类似 Microsoft Word 标题栏的东西。
我知道如何创建一个 wxButton,我也知道如何为它设置一个图标。但是我不知道如何使用原生 OS 图标或主题。
wxButton* closeButton = new wxButton(this, wxID_ANY, "x"); // how to tell that be like OS close button!
在 WinAPI 中,有一个名为 DrawThemeBackground 的函数,我可以将其与 WP_CLOSEBUTTON
一起使用,但我不知道 wxWidgets 中的等价物是什么。
更新:在大家的帮助下,这是在Windows中绘制本机按钮的示例代码(在其他OS中不起作用)。不幸的是,结果不是我想要的。它看起来像 Win XP 图标。 wxNativeRenderer 似乎不能正常工作。有人知道修复此代码吗? (是的,我添加了“wx.rc”资源文件并且我没有使用任何清单)
// wxWidgets "Hello World" Program
// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
#include <wx/renderer.h>
#include <wx/artprov.h>
class MyApp: public wxApp
{
public:
virtual bool OnInit();
};
class MyFrame: public wxFrame
{
public:
MyFrame();
private:
};
wxIMPLEMENT_APP( MyApp );
bool MyApp::OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show( true );
return true;
}
wxBitmap getButtonBitmap( wxWindow* win, wxTitleBarButton type, const wxColour& bg, int flags = 0 )
{
const wxSize sizeBmp = wxArtProvider::GetSizeHint( wxART_BUTTON );
wxBitmap bmp( sizeBmp );
wxMemoryDC dc( bmp );
dc.SetBackground( bg );
dc.Clear();
wxRendererNative::Get().DrawTitleBarBitmap( win, dc, sizeBmp, type, flags );
return bmp;
}
MyFrame::MyFrame()
: wxFrame( NULL, wxID_ANY, "Hello World" )
{
wxWindow* win = this;
wxColour color = win->GetBackgroundColour();
// minimize button
wxBitmapButton* minimizeButton = new wxBitmapButton( win, wxID_ANY,
getButtonBitmap( win, wxTITLEBAR_BUTTON_ICONIZE, color ),
wxPoint( 0, 0 ), wxDefaultSize, wxBORDER_NONE );
minimizeButton->SetBitmapPressed( getButtonBitmap( win, wxTITLEBAR_BUTTON_ICONIZE, color, wxCONTROL_PRESSED ) );
minimizeButton->SetBitmapCurrent( getButtonBitmap( win, wxTITLEBAR_BUTTON_ICONIZE, color, wxCONTROL_CURRENT ) );
// maximize button
wxBitmapButton* maximizeButton = new wxBitmapButton( win, wxID_ANY,
getButtonBitmap( win, wxTITLEBAR_BUTTON_MAXIMIZE, color ),
wxPoint( 30, 0 ), wxDefaultSize, wxBORDER_NONE );
maximizeButton->SetBitmapPressed( getButtonBitmap( win, wxTITLEBAR_BUTTON_MAXIMIZE, color, wxCONTROL_PRESSED ) );
maximizeButton->SetBitmapCurrent( getButtonBitmap( win, wxTITLEBAR_BUTTON_MAXIMIZE, color, wxCONTROL_CURRENT ) );
// close Button
wxBitmapButton* closeButton = new wxBitmapButton( win, wxID_ANY,
getButtonBitmap( win, wxTITLEBAR_BUTTON_CLOSE, color ),
wxPoint( 60, 0 ), wxDefaultSize, wxBORDER_NONE );
closeButton->SetBitmapPressed( getButtonBitmap( win, wxTITLEBAR_BUTTON_CLOSE, color, wxCONTROL_PRESSED ) );
closeButton->SetBitmapCurrent( getButtonBitmap( win, wxTITLEBAR_BUTTON_CLOSE, color, wxCONTROL_CURRENT ) );
}
您应该能够使用这些按钮的原生外观绘制位图。然后在 wxButton 上使用这些位图。
有一些限制,但Windows应该对它们有最好的支持。见 wxRendererNative
::DrawTitleBarBitmap()
.
绘制按钮(如标题栏上的按钮)的过程比简单地使用 DrawThemeBackground
函数要复杂一些。这是一个演示,它部分展示了如何使用 wxWidgets 执行此操作:
#include "wx/wx.h"
#include <wx/dcclient.h>
#include <wx/mstream.h>
#include <wx/dcmemory.h>
#include <wx/rawbmp.h>
#include <wx/msw/wrapwin.h>
#include <uxtheme.h>
#include <Vssym32.h>
#include <map>
// Helper data types
struct BGInfo
{
wxRect BgRect;
wxRect SizingMargins;
wxRect ContentMargins;
int TotalStates;
};
struct ButtonInfo
{
wxRect ButtonRect;
int TotalStates;
};
enum class DPI
{
dpi96 = 0,
dpi120,
dpi144,
dpi196
};
enum class Button
{
Close = 0,
Min,
Max,
Restore,
Help
};
// Helper functions
void MarginsToRect(const MARGINS& m, wxRect& r)
{
r.SetLeft(m.cxLeftWidth);
r.SetRight(m.cxRightWidth);
r.SetTop(m.cyTopHeight);
r.SetBottom(m.cyBottomHeight);
}
void RectTowxRect(const RECT & r, wxRect& r2)
{
r2.SetLeft(r.left);
r2.SetTop(r.top);
r2.SetRight(r.right-1);
r2.SetBottom(r.bottom-1);
}
wxBitmap ExtractAtlas(const wxBitmap& atlas, int total, int loc)
{
int bgheight = atlas.GetHeight();
int individualHeight = bgheight/total;
int bgWidth = atlas.GetWidth();
int atlasOffset = individualHeight*loc;
wxRect bgRect = wxRect(wxPoint(0,atlasOffset),
wxSize(bgWidth,individualHeight));
return atlas.GetSubBitmap(bgRect);
}
void TileBitmap(const wxBitmap& bmp, wxDC& dc, const wxRect& r)
{
dc.SetClippingRegion(r);
for ( int y = 0 ; y < r.GetHeight() ; y += bmp.GetHeight() )
{
for ( int x = 0 ; x < r.GetWidth() ; x += bmp.GetWidth() )
{
dc.DrawBitmap(bmp, r.GetLeft() + x, r.GetTop() + y, true);
}
}
dc.DestroyClippingRegion();
}
void TileTo(const wxBitmap& in, const wxRect& margins, wxBitmap& out, int w, int h)
{
// Theoretically we're supposed to split the bitmap into 9 pieces based on
// the sizing margins and leave the 8 outside pieces as unchanged as
// possible and the fill the remainder with the center piece. However doing
// that doesn't look actual control buttons. So I'm going to just tile
// the center bitmap to fill the whole space.
int ml = margins.GetLeft();
int mr = margins.GetRight();
int mt = margins.GetTop();
int mb = margins.GetBottom();
int bw = in.GetWidth();
int bh = in.GetHeight();
wxBitmap center = in.GetSubBitmap(wxRect(wxPoint( ml,mt),wxSize(bw-ml-mr,bh-mb-mt)));
// Create and initially transparent bitmap.
unsigned char* data = reinterpret_cast<unsigned char*>(malloc(3*w*h));
unsigned char* alpha = reinterpret_cast<unsigned char*>(malloc(w*h));
memset(alpha, 0, w*h);
wxImage im(w, h, data, alpha);
wxBitmap bmp(im);
wxMemoryDC dc(bmp);
TileBitmap(center, dc, wxRect(wxPoint(0,0),wxSize(w,h)));
dc.SelectObject(wxNullBitmap);
out = bmp;
}
class MyFrame: public wxFrame
{
public:
MyFrame();
private:
void OnPaintImagePanel(wxPaintEvent&);
void OnListSelection(wxCommandEvent&);
void BuildItemToDraw();
void LoadThemeData();
wxListBox* m_typeBox, *m_dpiBox, *m_stateBox;
wxPanel* m_imagePanel;
wxBitmap m_fullAtlas;
wxBitmap m_itemToDraw;
BGInfo m_closeInfo;
BGInfo m_otherInfo;
std::map<std::pair<DPI,Button>,ButtonInfo> m_themeMap;
};
MyFrame::MyFrame():wxFrame(NULL, wxID_ANY, "Windows Control Button Demo", wxDefaultPosition,
wxSize(400, 300))
{
// Start all the image handlers. Only the PNG handler is actually needed.
::wxInitAllImageHandlers();
// Build the UI.
wxPanel* bg = new wxPanel(this, wxID_ANY);
wxStaticText* typeText = new wxStaticText(bg,wxID_ANY,"Type:");
m_typeBox = new wxListBox(bg,wxID_ANY);
wxStaticText* dpiText = new wxStaticText(bg,wxID_ANY,"dpi:");
m_dpiBox = new wxListBox(bg,wxID_ANY);
wxStaticText* stateText = new wxStaticText(bg,wxID_ANY,"State:");
m_stateBox = new wxListBox(bg,wxID_ANY);
m_imagePanel = new wxPanel(bg,wxID_ANY);
wxBoxSizer* mainSzr = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* boxSzr = new wxBoxSizer(wxHORIZONTAL);
boxSzr->Add(typeText, wxSizerFlags().Border(wxALL));
boxSzr->Add(m_typeBox, wxSizerFlags().Border(wxTOP|wxRIGHT|wxBOTTOM));
boxSzr->Add(dpiText, wxSizerFlags().Border(wxALL));
boxSzr->Add(m_dpiBox, wxSizerFlags().Border(wxTOP|wxRIGHT|wxBOTTOM));
boxSzr->Add(stateText, wxSizerFlags().Border(wxALL));
boxSzr->Add(m_stateBox, wxSizerFlags().Border(wxTOP|wxRIGHT|wxBOTTOM));
mainSzr->Add(boxSzr,wxSizerFlags());
mainSzr->Add(m_imagePanel,wxSizerFlags(1).Expand().Border(wxLEFT|wxRIGHT|wxBOTTOM));
bg->SetSizer(mainSzr);
// Set the needed event handlers for the controls.
m_imagePanel->Bind(wxEVT_PAINT, &MyFrame::OnPaintImagePanel, this);
m_typeBox->Bind(wxEVT_LISTBOX, &MyFrame::OnListSelection, this);
m_dpiBox->Bind(wxEVT_LISTBOX, &MyFrame::OnListSelection, this);
m_stateBox->Bind(wxEVT_LISTBOX, &MyFrame::OnListSelection, this);
// Concigure the controls.
m_typeBox->Append("Close");
m_typeBox->Append("Help");
m_typeBox->Append("Max");
m_typeBox->Append("Min");
m_typeBox->Append("Restore");
m_dpiBox->Append("96");
m_dpiBox->Append("120");
m_dpiBox->Append("144");
m_dpiBox->Append("192");
m_stateBox->Append("Normal");
m_stateBox->Append("Hot");
m_stateBox->Append("Pressed");
m_stateBox->Append("Inactive");
m_typeBox->Select(0);
m_dpiBox->Select(0);
m_stateBox->Select(0);
// Load the theme data and finish setting up.
LoadThemeData();
BuildItemToDraw();
}
void MyFrame::LoadThemeData()
{
HINSTANCE handle = LoadLibraryEx(L"C:\Windows\Resources\Themes\aero\aero.msstyles",
0, LOAD_LIBRARY_AS_DATAFILE);
if ( handle == NULL )
{
return;
}
HTHEME theme = OpenThemeData(reinterpret_cast<HWND>(this->GetHandle()),L"DWMWindow");
VOID* PBuf = NULL;
DWORD BufSize = 0;
GetThemeStream(theme, 0,0, TMT_DISKSTREAM, &PBuf, &BufSize, handle);
wxMemoryInputStream mis(PBuf,static_cast<int>(BufSize));
wxImage im(mis, wxBITMAP_TYPE_PNG);
if ( !im.IsOk() )
{
return;
}
wxBitmap b2(im);
m_fullAtlas = wxBitmap(im);;
MARGINS m;
RECT r;
int BUTTONACTIVECAPTION = 3;
int BUTTONACTIVECLOSE = 7;
int BUTTONCLOSEGLYPH96 = 11;
int BUTTONRESTOREGLYPH192 = 30;
// Store some of the theme info for the parts BUTTONACTIVECAPTION
// and BUTTONACTIVECLOSE.
GetThemeRect(theme, BUTTONACTIVECAPTION, 0, TMT_ATLASRECT, &r);
RectTowxRect(r,m_otherInfo.BgRect);
GetThemeMargins(theme,NULL, BUTTONACTIVECAPTION,0, TMT_CONTENTMARGINS,NULL, &m);
MarginsToRect(m,m_otherInfo.ContentMargins);
GetThemeMargins(theme,NULL, BUTTONACTIVECAPTION,0, TMT_SIZINGMARGINS,NULL, &m);
MarginsToRect(m,m_otherInfo.SizingMargins);
GetThemeInt(theme, BUTTONACTIVECAPTION, 0, TMT_IMAGECOUNT, &(m_otherInfo.TotalStates));
GetThemeRect(theme, BUTTONACTIVECLOSE, 0, TMT_ATLASRECT, &r);
RectTowxRect(r,m_closeInfo.BgRect);
GetThemeMargins(theme,NULL, BUTTONACTIVECLOSE,0, TMT_CONTENTMARGINS,NULL, &m);
MarginsToRect(m,m_closeInfo.ContentMargins);
GetThemeMargins(theme,NULL, BUTTONACTIVECLOSE,0, TMT_SIZINGMARGINS,NULL, &m);
MarginsToRect(m,m_closeInfo.SizingMargins);
GetThemeInt(theme, BUTTONACTIVECLOSE, 0, TMT_IMAGECOUNT, &(m_closeInfo.TotalStates));
// Since the part numbers for BUTTONCLOSEGLYPH96..BUTTONRESTOREGLYPH192
// are all sequential and the dpis all run from 96 to 192 in the same
// order, we can use a for loop to store
for ( int i = BUTTONCLOSEGLYPH96 ; i <= BUTTONRESTOREGLYPH192 ; ++i )
{
int j = i-BUTTONCLOSEGLYPH96;
Button b = static_cast<Button>(j/4);
DPI dpi = static_cast<DPI>(j%4);
std::pair<DPI,Button> item;
ButtonInfo info;
item = std::make_pair(dpi,b);
GetThemeRect(theme, i, 0, TMT_ATLASRECT, &r);
RectTowxRect(r,info.ButtonRect);
GetThemeInt(theme, i, 0, TMT_IMAGECOUNT, &(info.TotalStates));
m_themeMap.insert(std::make_pair(item,info));
}
CloseThemeData(theme);
FreeLibrary(handle);
}
void MyFrame::OnPaintImagePanel(wxPaintEvent&)
{
wxPaintDC dc(m_imagePanel);
dc.Clear();
if ( m_itemToDraw.IsOk() )
{
dc.DrawBitmap(m_itemToDraw,0,0,true);
}
}
void MyFrame::OnListSelection(wxCommandEvent&)
{
BuildItemToDraw();
}
void MyFrame::BuildItemToDraw()
{
BGInfo bginfo;
Button b = static_cast<Button>(m_typeBox->GetSelection());
DPI dpi = static_cast<DPI>(m_dpiBox->GetSelection());
int state = m_stateBox->GetSelection();
if ( b == Button::Close )
{
bginfo = m_closeInfo;
}
else
{
bginfo = m_otherInfo;
}
wxBitmap bgAtlas = m_fullAtlas.GetSubBitmap(bginfo.BgRect);
int totalbgs = bginfo.TotalStates;
wxBitmap bg = ExtractAtlas(bgAtlas, totalbgs, state);
std::pair<DPI,Button> item = std::make_pair(dpi,b);
auto it = m_themeMap.find(item);
if ( it != m_themeMap.end() )
{
ButtonInfo info = it->second;
wxBitmap itemAtlas = m_fullAtlas.GetSubBitmap(info.ButtonRect);
wxBitmap item = ExtractAtlas(itemAtlas, info.TotalStates, state);
wxRect contentmargins = bginfo.ContentMargins;
wxRect Sizingmargins = bginfo.SizingMargins;
int width = item.GetWidth() + contentmargins.GetLeft() + contentmargins.GetRight();
int height = item.GetHeight() + contentmargins.GetTop() + contentmargins.GetBottom();
if ( bg.GetWidth() > width )
{
width = bg.GetWidth();
}
if ( bg.GetHeight() > height )
{
height = bg.GetHeight();
}
wxBitmap bmp(width,height,32);
TileTo(bg,Sizingmargins, bmp, width, height);
wxMemoryDC dc(bmp);
int leftOffset = (width-item.GetWidth())/2;
int topOffset = (height - item.GetHeight())/2;
dc.DrawBitmap(item,leftOffset,topOffset, true);
dc.SelectObject(wxNullBitmap);
m_itemToDraw = bmp;
}
m_imagePanel->Refresh();
m_imagePanel->Update();
}
class MyApp : public wxApp
{
public:
virtual bool OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(MyApp);
这只是部分答案,因为,
- 这依赖于我只是输入代码的
BUTTONACTIVECAPTION
之类的数字部件号。这些数字最终是从文件 Aero.msstyles 中提取的,理论上,如果 Microsoft 更改该文件,代码中的数字可能是错误的。一个完整的答案将查看该文件并从中提取正确的数字,以便它始终可以确保它使用的是正确的数字。但是这样做超出了这个答案的范围。 - 我不确定如何获得按钮的大小。在我的系统上,关闭按钮的宽度为 45 像素,高度为 29 像素。但是我在任何主题数据中都看不到这些数字。
绘制这些按钮的技巧是您首先必须将主题文件作为 dll 打开。可以使用条目 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ThemeManager\DllName
从注册表中提取主题文件的名称。在上面的代码中,我只是将其硬编码为“C:\Windows\Resources\Themes\aero\aero.msstyles”,但从注册表中提取它可能比硬编码文件名更好。
打开主题后,特殊技巧是调用GetThemeStream函数。此 returns 内存中的 png 文件。它的第一部分如下所示:
如您所见,此 png 包含一堆控制按钮的图片。我们需要使用 GetThemeRect 函数来了解此 png 中与我们要绘制的部分对应的矩形。
但是现在我们运行遇到了问题。我们需要使用的主题class是“DWMWindow”。这个 class 完全没有文档,了解它的部分的唯一方法是使用像 msstyleEditor 这样的程序来查看主题文件。
从程序中可以看出我们感兴趣的零件号是:
int BUTTONACTIVECAPTION = 3;
int BUTTONACTIVECLOSE = 7;
int BUTTONCLOSEGLYPH96 = 11;
int BUTTONCLOSEGLYPH120 = 12;
int BUTTONCLOSEGLYPH144 = 13;
int BUTTONCLOSEGLYPH192 = 14;
int BUTTONHELPGLYPH96 = 15;
int BUTTONHELPGLYPH120 = 16;
int BUTTONHELPGLYPH144 = 17;
int BUTTONHELPGLYPH192 = 18;
int BUTTONMAXGLYPH96 = 19;
int BUTTONMAXGLYPH120 = 20;
int BUTTONMAXGLYPH144 = 21;
int BUTTONMAXGLYPH192 = 22;
int BUTTONMINGLYPH96 = 23;
int BUTTONMINGLYPH120 = 24;
int BUTTONMINGLYPH144 = 25;
int BUTTONMINGLYPH192 = 26;
int BUTTONRESTOREGLYPH96 = 27;
int BUTTONRESTOREGLYPH120 = 28;
int BUTTONRESTOREGLYPH144 = 29;
int BUTTONRESTOREGLYPH192 = 30;
有了这些部件号,我们可以使用 GetThemeRect
函数知道 png 的哪些部分用于我们要绘制的项目。
还有一些最后的问题。 GetThemeRect returns 为部分 BUTTONCLOSEGLYPH96 = 11
提供的矩形如下所示:
这称为图集,该子矩形中的 4 个部分分别对应于状态正常、热、推送和禁用。然而,由于 class 没有记录,唯一知道的方法是查看 msstyleEditor 的输出或从主题中获取它是其他方式。幸运的是,我们可以使用 GetThemeInt
和 TMT_IMAGECOUNT
属性 标识符来获取图集中的图像数量,这样至少我们知道要将其切成多少块。
我们可以从主题数据中提取更多信息。 GetThemeMargins
和 TMT_SIZINGMARGINS
属性 id 应该告诉我们如何将背景图像平铺成更大的尺寸。然而,在我的实验中,这些边缘的数字似乎并没有给出好的结果。因此,在上面的代码中,我只是平铺了中心部分来填充整个背景。此外,使用 TMT_CONTENTMARGINS
属性 id 应该可以告诉我们将字形放置在背景上的什么位置。但同样,在我的实验中,这些位置看起来不太好。所以在上面的代码中,我只是将字形置于背景的中心。
将所有这些放在一起,我们最终可以绘制关闭、最小值、最大值和恢复按钮,因为它们出现在标题栏上。