如何在 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);

这只是部分答案,因为,

  1. 这依赖于我只是输入代码的 BUTTONACTIVECAPTION 之类的数字部件号。这些数字最终是从文件 Aero.msstyles 中提取的,理论上,如果 Microsoft 更改该文件,代码中的数字可能是错误的。一个完整的答案将查看该文件并从中提取正确的数字,以便它始终可以确保它使用的是正确的数字。但是这样做超出了这个答案的范围。
  2. 我不确定如何获得按钮的大小。在我的系统上,关闭按钮的宽度为 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 的输出或从主题中获取它是其他方式。幸运的是,我们可以使用 GetThemeIntTMT_IMAGECOUNT 属性 标识符来获取图集中的图像数量,这样至少我们知道要将其切成多少块。

我们可以从主题数据中提取更多信息。 GetThemeMarginsTMT_SIZINGMARGINS 属性 id 应该告诉我们如何将背景图像平铺成更大的尺寸。然而,在我的实验中,这些边缘的数字似乎并没有给出好的结果。因此,在上面的代码中,我只是平铺了中心部分来填充整个背景。此外,使用 TMT_CONTENTMARGINS 属性 id 应该可以告诉我们将字形放置在背景上的什么位置。但同样,在我的实验中,这些位置看起来不太好。所以在上面的代码中,我只是将字形置于背景的中心。

将所有这些放在一起,我们最终可以绘制关闭、最小值、最大值和恢复按钮,因为它们出现在标题栏上。