Home > other >  How to create control buttons exactly like Window's (OS) control buttons in wxWidgets?
How to create control buttons exactly like Window's (OS) control buttons in wxWidgets?

Time:10-12

I want to create control buttons (minimize, maximize and close) exaclty like Windows.

I know how to create a wxButton and also I know how to set an icon for it. However I don't know how to use native OS icons or theme.

wxButton* closeButton = new wxButton(this, wxID_ANY, "x"); // how to tell that be like OS close button!

In WinAPI, there is a function called enter image description here

#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, 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);

This is only a partial answer because,

  1. This relys on numbers part numbers like BUTTONACTIVECAPTION that I'm simply entering into the code. These numbers are ultimately pulled from the file Aero.msstyles, and theoretically if Microsoft changes that file, the numbers in the code could be wrong. A full answer would look at that file and pull out the correct numbers from it so that it can always be sure it's using the correct ones. But doing that is beyond the scope of this answer.
  2. I'm not sure how the get the size for the buttos. On my system, the close button has a width of 45 pixels and height of 29 pixels. But I don't see those numers anywhere in any of the theme data.

The trick to drawing these buttons is that you first have to open the theme file as a dll. The name for the theme file can be pulled from the registry with the entry HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ThemeManager\DllName. In the code above, I just hard coded this as "C:\Windows\Resources\Themes\aero\aero.msstyles", but it would probably be better to pull that from the registry instead of hard coding the filename.

Once the theme is opened, the special trick is to call the enter image description here

As you can see, this png contains a bunch of the pices of the control buttons. We'll need to use the GetThemeRect function to learn the rectangles in this png that coorespond to the parts that we want to draw.

But now we run into a problem. The theme class we need to use is "DWMWindow". This class is completely undocumented and the only way to learn its parts is to use a program like enter image description here.

From the program, we can see that the part numbers we are interested in are:

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;

With those part numbers we can use the GetThemeRect function know which parts of the png to use for the item we want to draw.

There's still some final problems. The rectangles GetThemeRect returns give for part BUTTONCLOSEGLYPH96 = 11 looks like this:

enter image description here

This is called an atlas, and each of the 4 pieces in that subrectangle corresponds to the states normal, hot, pushed, and disabled. However, again since the class is undocumented, the only way to know that is to look at the output from msstyleEditor or getting it from the theme is some other way. Fortunately we can use the GetThemeInt with the TMT_IMAGECOUNT property identifier to get the number of images in the atlas so at least we know how many pieces to cut it into.

There are a few more pieces of information we can pull from the theme data. The GetThemeMargins with the TMT_SIZINGMARGINS property id should tell us how to tile the background images into larger sizes. However in my experiments, the numbers from those margins don't seem to give good results. Consequently, in the code above I just tiled the center part to fill the whole background. In addition, using the TMT_CONTENTMARGINS property id should tell us where to place the glyphs on the background. But again, in my experiments, those positions didn't look good. So in the code above, I just centered the glyphs on the background.

Putting all of this together, we can finally draw the close, min, max, and restore buttons as they appear on a title bar.

  • Related