Search code examples
c++winapiwxwidgets

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


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

The final goal is to create something like the title bar of Microsoft Word.

enter image description here

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 DrawThemeBackground which I can use with WP_CLOSEBUTTON but I don't know what's the equivalent in wxWidgets.

Update: With the help of all of you, this is a sample code for drawing native buttons in Windows (not work in other OS). Unfortunately, the result is not what I want. It looks like Win XP icons. It seems that wxNativeRenderer does not work properly. Does anybody have any idea to fix this code? (Yes, I have added "wx.rc" resource file and I don't use any manifest)

enter image description here

// 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 ) );
}

Solution

  • The process for drawing buttons like the ones on a title bar is a little more involved that simply using the DrawThemeBackground function. Here is a demo that partially shows how to do this using wxWidgets:

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

    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 GetThemeStream function. This returns an in memory png file. The first part of it looks like this:

    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 msstyleEditor to look at the theme file.

    Running that program looks like this, 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.