Tabs - like in Visual Studio

If you have a cool piece of software to share, but you are not hosting it officially yet, please dump it in here. If you have code snippets that are useful, please donate!
Post Reply
arucard
Experienced Solver
Experienced Solver
Posts: 61
Joined: Tue Dec 28, 2004 10:16 am
Location: Czech rep.

Tabs - like in Visual Studio

Post by arucard »

Hello, I have finished wxTabbedCtrl which shows and manages tabs similar to ms visual studio. Most of its methods are identical to wxNotebook. It can draw the bounding rectangle and closing button as well. Has also image support. Sends events if user chooses a tab (like wxNotebook does) or pushes the close button. The picture is here

Note: I have updated the code because of minor painting issue after control's size change

Header part:

Code: Select all

//hit results
enum {
   wxTB_HITTEST_NOWHERE = 0,   // not on tab
   wxTB_HITTEST_ONICON  = 1,   // on icon
   wxTB_HITTEST_ONLABEL = 2,   // on label
   wxTB_HITTEST_ONITEM  = 4,
};

#define wxTB_TOP     0x00000001
#define wxTB_BOTTOM  0x00000002
#define wxTB_X       0x00000010
#define wxTB_DEFAULT_STYLE wxTB_TOP|wxSTATIC_BORDER
//also wxSTATIC_BORDER and wxNO_BORDER are working here

class wxTabbedCtrl : public wxControl {

   DECLARE_DYNAMIC_CLASS(wxTabbedCtrl)
   
   DECLARE_EVENT_TABLE()
   
   class wxTabbedPage {
   public:
      wxString text;
      int image;
      wxTabbedPage(const wxString &t, int img)
         : text(t), image(img) {}
   };

   typedef std::vector<wxTabbedPage> pages_type;

   pages_type pages;
   int active;
   long style;
   wxImageList *img_list;
   wxSize padding;
   bool hover;
   wxRect x_rect;

   void OnMouse(wxMouseEvent &);
   void OnPaint(wxPaintEvent &);
   void OnSize(wxSizeEvent &);
   void OnEraseBackground(wxEraseEvent &);
   void DrawX(bool active, wxDC &dc);

public:
   wxTabbedCtrl();
   wxTabbedCtrl(wxWindow *parent, wxWindowID id,
      const wxPoint &position = wxDefaultPosition, const wxSize size = wxDefaultSize,
      long style = wxTB_DEFAULT_STYLE, const wxString &name = "TabbedCtrl") 
   {
      Create(parent, id, position, size, style, name);
   }
   void Create(wxWindow *parent, wxWindowID id,
      const wxPoint &position = wxDefaultPosition, const wxSize &size = wxDefaultSize,
      long style = wxTB_DEFAULT_STYLE, const wxString &name = "TabbedCtrl");
      
   virtual ~wxTabbedCtrl() {}

   void AddPage(const wxString &text, bool select = false, int img = -1);
   void InsertPage(int pg, const wxString& text, bool select = false, int img = -1);
   void DeleteAllPages();
   void DeletePage(int pg);
   int GetPageCount() { return pages.size(); }
   int GetSelection() { return active; }
   int HitTest(const wxPoint &pos, long *flags = 0);
   void SetSelection(int pg);
   wxString GetPageText(int pg);
   void SetPageText(int pg, const wxString &t);
   int GetPageImage(int pg);
   void SetPageImage(int pg, int img);
   wxImageList *GetImageList() { return img_list; }
   void SetImageList(wxImageList *list) { img_list = list; }
   wxSize GetPadding() { return padding; }
   void SetPadding(const wxSize &pad) { padding = pad; }
};

class wxTabbedCtrlEvent : public wxNotifyEvent {
   DECLARE_DYNAMIC_CLASS(wxTabbedCtrlEvent)

   size_t sel, oldsel;

public:
   wxTabbedCtrlEvent(wxEventType commandType = wxEVT_NULL, int winid = 0, int nSel = -1, int nOldSel = -1)
    : wxNotifyEvent(commandType, winid), sel(nSel), oldsel(nOldSel)
   {}
   void SetSelection(int s) { sel = s; }
   void SetOldSelection(int s) { oldsel = s; }
   int GetSelection() { return sel; }
   int GetOldSelection() { return oldsel; }
};


BEGIN_DECLARE_EVENT_TYPES()
    DECLARE_EVENT_TYPE(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGED, 10000)
    DECLARE_EVENT_TYPE(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGING, 10001)
    DECLARE_EVENT_TYPE(wxEVT_COMMAND_TABBEDCTRL_PAGE_CLOSING, 10003)
END_DECLARE_EVENT_TYPES()

typedef void (wxEvtHandler::*wxTabbedCtrlEventFunction)(wxTabbedCtrlEvent&);

#define wxTabbedCtrlEventHandler(func) \
    (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(wxTabbedCtrlEventFunction, &func)

#define EVT_TABBEDCTRL_PAGE_CHANGED(winid, fn) \
    wx__DECLARE_EVT1(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGED, winid, wxTabbedCtrlEventHandler(fn))

#define EVT_TABBEDCTRL_PAGE_CHANGING(winid, fn) \
    wx__DECLARE_EVT1(wxEVT_COMMAND_TABEDDCTRL_PAGE_CHANGING, winid, wxTabbedCtrlEventHandler(fn))

#define EVT_TABBEDCTRL_PAGE_CLOSING(winid, fn) \
    wx__DECLARE_EVT1(wxEVT_COMMAND_TABBEDCTRL_PAGE_CLOSING, winid, wxTabbedCtrlEventHandler(fn))
Source part:

Code: Select all

//////////////////////////////////////////////////////////////////////////////////
// TabbedCtrl
//////////////////////////////////////////////////////////////////////////////////

IMPLEMENT_DYNAMIC_CLASS(wxTabbedCtrlEvent, wxNotifyEvent);

IMPLEMENT_DYNAMIC_CLASS(wxTabbedCtrl, wxControl);

DEFINE_EVENT_TYPE(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGED)
DEFINE_EVENT_TYPE(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGING) 
DEFINE_EVENT_TYPE(wxEVT_COMMAND_TABBEDCTRL_PAGE_CLOSING) 

BEGIN_EVENT_TABLE(wxTabbedCtrl, wxControl)
   EVT_PAINT(wxTabbedCtrl::OnPaint)
   EVT_LEFT_DOWN(wxTabbedCtrl::OnMouse)
   EVT_MOTION(wxTabbedCtrl::OnMouse)
   EVT_SIZE(wxTabbedCtrl::OnSize)
   EVT_ERASE_BACKGROUND(wxTabbedCtrl::OnEraseBackground)
END_EVENT_TABLE()

wxTabbedCtrl::wxTabbedCtrl() 
: active(-1), img_list(0), style(0) 
{
}  

void wxTabbedCtrl::Create(wxWindow *parent, wxWindowID id,
      const wxPoint &position, const wxSize &size,
      long style, const wxString &name) 
{
   wxWindow::Create(parent, id, position, size, wxNO_BORDER, name);
   active = -1;
   img_list = 0;
   this->style = style;
   padding.x = 5; 
   padding.y = 3; 
   hover = false;
}

void wxTabbedCtrl::AddPage(const wxString &text, bool select, int img) {
   pages.push_back(wxTabbedPage(text, img));
   if(select || GetSelection()==-1) SetSelection(GetPageCount()-1);
   else Refresh();
}

void wxTabbedCtrl::InsertPage(int pg, const wxString& text, bool select, int img) {
   wxASSERT_MSG(pg >= 0 && pg <= GetPageCount(), "Got invalid page number");
   pages_type::iterator it = pages.begin() + pg;
   pages.insert(it, wxTabbedPage(text, img));
   if(select || GetSelection()==-1) SetSelection(pg);
   else Refresh();
}

void wxTabbedCtrl::DeleteAllPages() {
   pages.clear();
   active = -1;
   Refresh();
}

void wxTabbedCtrl::DeletePage(int pg) {
   wxASSERT_MSG(pg >= 0 && pg < GetPageCount(), "Got invalid page number");
   pages_type::iterator it = pages.begin() + pg;
   pages.erase(it);
   if(pg < active) active--;
   else if(active==pg && active==GetPageCount()) active--;
   Refresh();
}

void wxTabbedCtrl::SetSelection(int pg) {
   wxASSERT_MSG(pg >= 0 && pg < GetPageCount(), "Got invalid page number");
   if(pg != active) {
      wxTabbedCtrlEvent event(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGING, m_windowId);
      event.SetSelection(pg);
      event.SetOldSelection(active);
      event.SetEventObject(this);
      if(!GetEventHandler()->ProcessEvent(event) || event.IsAllowed())
      { 
         // program allows the page change
         active = pg;
         event.SetEventType(wxEVT_COMMAND_TABBEDCTRL_PAGE_CHANGED);
         event.SetOldSelection(active);
         GetEventHandler()->ProcessEvent(event);
         Refresh();
      }
   }
}

wxString wxTabbedCtrl::GetPageText(int pg) {
   wxASSERT_MSG(pg >= 0 && pg < GetPageCount(), "Got invalid page number");
   return pages[pg].text;
}

void wxTabbedCtrl::SetPageText(int pg, const wxString &t) {
   wxASSERT_MSG(pg >= 0 && pg < GetPageCount(), "Got invalid page number");
   if(pages[pg].text != t) {
      pages[pg].text = t;
      Refresh();
   }
}

int wxTabbedCtrl::GetPageImage(int pg) {
   wxASSERT_MSG(pg >= 0 && pg < GetPageCount(), "Got invalid page number");
   return pages[pg].image;
}

void wxTabbedCtrl::SetPageImage(int pg, int img) {
   wxASSERT_MSG(pg >= 0 && pg < GetPageCount(), "Got invalid page number");
   if(pages[pg].image != img) {
      pages[pg].image = img;
      Refresh();
   }
}

int wxTabbedCtrl::HitTest(const wxPoint &p, long *flags) {
   int height, width, pom;
   bool mirror = style & wxTB_BOTTOM;
   bool drawx = style & wxTB_X;
   wxSize size = GetSize();
   wxClientDC dc(this);

   wxFont normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
   wxFont bold_font = normal_font;
   bold_font.SetWeight(wxFONTWEIGHT_BOLD);
   
   if(flags) *flags = wxTB_HITTEST_NOWHERE;
   dc.SetFont(bold_font);
   dc.GetTextExtent("Aq", &pom, &height);
   if(p.x <= 0 || p.x >= size.x) return wxNOT_FOUND;
   if(!mirror && (p.y <= size.y-height-padding.y*2 || p.y >= size.y)) return wxNOT_FOUND;
   if(mirror && (p.y <= 0 || p.y >= height+padding.y*2)) return wxNOT_FOUND;

   int posx = 3;
   for(int i = 0; i < GetPageCount(); i++) {
      dc.SetFont((i==GetSelection()) ? bold_font : normal_font);
      dc.GetTextExtent(GetPageText(i), &width, &pom);
      
      wxBitmap bmp;
      if(GetPageImage(i) >= 0) bmp = img_list->GetBitmap(i);
      int space = padding.x;
      if(bmp.Ok()) space += bmp.GetWidth()+padding.x;

      if(p.x > posx && p.x < posx+width+space+padding.x) {
         if(flags) *flags = wxTB_HITTEST_ONITEM;
         
         //onicon attempt 
         if(flags && bmp.Ok() && p.x >= posx+padding.x && p.x <= posx+bmp.GetWidth()+padding.x) {
            if(!mirror && p.y >= size.y-height-padding.y && p.y <= size.y-padding.y) *flags = wxTB_HITTEST_ONICON;
            else if(mirror && p.y >= padding.y && p.y <= padding.y+bmp.GetHeight()) *flags = wxTB_HITTEST_ONICON;
         }
         //onlabel attempt
         else if(flags && p.x >= posx+space && p.x <= posx+space+width) {
            if(!mirror && p.y >= size.y-height-padding.y && p.y <= size.y-padding.y) *flags = wxTB_HITTEST_ONLABEL;
            else if(mirror && p.y >= padding.y && p.y <= padding.y+height) *flags = wxTB_HITTEST_ONLABEL;
         }
         
         return i;
      }
      
      posx += width+space+padding.x;
   }
   
   return wxNOT_FOUND;
}

void wxTabbedCtrl::OnMouse(wxMouseEvent &ev) {
   if(ev.GetEventType()==wxEVT_MOTION) {
      wxPoint mouse = ev.GetPosition();
      bool nhover = mouse.x >= x_rect.x && mouse.x <= x_rect.x+x_rect.width && mouse.y >= x_rect.y && mouse.y <= x_rect.y+x_rect.height;
      if(hover != nhover) {
         hover = nhover;
         wxClientDC dc(this);
         DrawX(hover, dc);
      }
   }
   else if(ev.GetEventType()==wxEVT_LEFT_DOWN) {
      if(hover) {
         wxTabbedCtrlEvent event(wxEVT_COMMAND_TABBEDCTRL_PAGE_CLOSING, m_windowId);
         event.SetSelection(active);
         event.SetEventObject(this);
         GetEventHandler()->ProcessEvent(event);
      }
      else {
         int page = HitTest(ev.GetPosition());  
         if(page != wxNOT_FOUND) SetSelection(page);
      }
   }
}

void wxTabbedCtrl::OnSize(wxSizeEvent &) {
   Refresh();
}

void wxTabbedCtrl::OnEraseBackground(wxEraseEvent &) {
}

void wxTabbedCtrl::DrawX(bool active, wxDC &dc) {
   const int SIZE = 8;
   wxSize size = GetSize();
   wxBrush back_brush = wxBrush(GetBackgroundColour());
   wxPen back_pen = wxPen(GetBackgroundColour());
   wxPen x_pen = wxPen(active ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW) : *wxBLACK);
   x_pen.SetWidth(2);
   
   int posx = size.x-SIZE*2, posy = (size.y-SIZE)/2;
   x_rect = wxRect(posx, posy, SIZE, SIZE);

   dc.SetPen(back_pen);
   dc.SetBrush(back_brush);
   dc.DrawRectangle(posx-SIZE+1, 1, SIZE*3-2, size.y-2);

   dc.SetPen(x_pen);
   dc.DrawLine(posx, posy, posx+SIZE, posy+SIZE);
   dc.DrawLine(posx, posy+SIZE, posx+SIZE, posy);
}

void wxTabbedCtrl::OnPaint(wxPaintEvent &) {
   wxPaintDC dc(this);
   wxSize size = GetSize();
   wxBrush back_brush = wxBrush(GetBackgroundColour());
   wxBrush nosel_brush = wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
   wxBrush sel_brush = wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNHIGHLIGHT));
   wxPen border_pen = wxPen(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW));
   wxPen sel_pen = wxPen(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNHIGHLIGHT));
   wxPen back_pen = wxPen(GetBackgroundColour());
   wxFont normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
   wxFont bold_font = normal_font;
   bold_font.SetWeight(wxFONTWEIGHT_BOLD);
   bool mirror = style & wxTB_BOTTOM;
   bool fullborder = !(style & wxNO_BORDER);
   bool drawx = style & wxTB_X;
   
   dc.BeginDrawing();

   //background
   dc.SetTextBackground(GetBackgroundColour());
   dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT));
   dc.SetBrush(back_brush);
   if(fullborder) {
      dc.SetPen(border_pen);
      dc.DrawRectangle(0, 0, size.x, size.y);
   }
   else {
      dc.SetPen(back_pen);
      dc.DrawRectangle(0, 0, size.x, size.y);
      dc.SetPen(border_pen);
      dc.DrawLine(0, mirror ? 0 : size.y-1, size.x, mirror ? 0 : size.y-1);
   }

   int height, width, pom;
   dc.SetFont(bold_font);
   dc.GetTextExtent("Aq", &pom, &height);
   int posx = 3;
   
   //and tabs
   for(int i = 0; i < GetPageCount(); i++) {
      dc.SetPen(border_pen);
      dc.SetFont((i==GetSelection()) ? bold_font : normal_font);
      dc.SetBrush((i==GetSelection()) ? sel_brush : nosel_brush);
      dc.GetTextExtent(GetPageText(i), &width, &pom);
         
      wxBitmap bmp;
      if(GetPageImage(i) >= 0) bmp = img_list->GetBitmap(GetPageImage(i));
      
      int space = padding.x;
      if(bmp.Ok()) space += padding.x + bmp.GetWidth();
      
      if(!mirror) {
         dc.DrawRoundedRectangle(posx, size.y-height-padding.y*2, width+space+padding.x, height+padding.y*2+3, 3);
         dc.DrawText(GetPageText(i), posx+space, size.y-height-padding.y);
         if(i!=GetSelection()) dc.DrawLine(posx, size.y-1, posx+width+space+padding.x, size.y-1);

         if(bmp.Ok()) dc.DrawBitmap(bmp, posx+padding.x, size.y-(height+2*padding.y+bmp.GetHeight())/2, true);
      }
      else {
         dc.DrawRoundedRectangle(posx, -3, width+space+padding.x, height+padding.y*2+3, 3);
         dc.DrawText(GetPageText(i), posx+space, padding.y);
         if(i!=GetSelection()) dc.DrawLine(posx, 0, posx+width+space+padding.x, 0);

         if(bmp.Ok()) dc.DrawBitmap(bmp, posx+padding.x, (height+2*padding.y-bmp.GetHeight())/2, true);
      }

      posx += width+space+padding.x;
   }

   //X
   if(drawx) DrawX(hover, dc);

   dc.EndDrawing();
}
Last edited by arucard on Thu Aug 25, 2005 4:30 pm, edited 1 time in total.
wxWidgets 2.8.4, winxp, VC2003 and above
User avatar
kolo
Earned some good credits
Earned some good credits
Posts: 120
Joined: Tue Jun 21, 2005 1:19 pm
Location: Russia, Cheboksary
Contact:

Post by kolo »

if you post some code of using your class - it will be good ))
only MSW & MSVS ))
ssigala
Earned some good credits
Earned some good credits
Posts: 109
Joined: Fri Sep 03, 2004 9:30 am
Location: Brescia, Italy

Post by ssigala »

Thanks very much arucard!
Sandro Sigala - Kynosoft, Brescia
arucard
Experienced Solver
Experienced Solver
Posts: 61
Joined: Tue Dec 28, 2004 10:16 am
Location: Czech rep.

Post by arucard »

if you post some code of using your class - it will be good ))
Ok, I do but there is not much to tell about. The only significant difference between this class and wxNotebook is that if you are adding new page you don't need any wxWindow instance for the page itself. It's because wxTabbedCtrl doesn't have any pages, only tabs.

So for creating tabs with close button and bounding border simply call the constructor like this:

Code: Select all

Tabs = new wxTabbedCtrl(ParentWindow, ID_TABS, wxDefaultPosition, wxDefaultSize, wxSTATIC_BORDER|wxTB_TOP|wxTB_X);
Then you probably add it to the sizer. Don't forget to call something like this for setting the right size :

Code: Select all

Tabs->SetSizeHints(wxSize(-1, 22));
Now you are ready for working with tabs:

Code: Select all

Tabs->AddPage("First page"); //For adding 1 page without icon
Tabs->DeleteAllPages(); //Call this for deleting all pages
Tabs->SetImageList(SomeImageist); //Associate ImageList for icons in tabs
For reacting on events use these macros in your event table:

Code: Select all

EVT_TABBEDCTRL_PAGE_CHANGED //if user changed the selected page
EVT_TABBEDCTRL_PAGE_CLOSING //if user pressed the close button
And connect them to any method with wxTabbedCtrlEvent & parametr as usual

I think other things are obvious from the Header part
Last edited by arucard on Thu Aug 25, 2005 4:32 pm, edited 1 time in total.
wxWidgets 2.8.4, winxp, VC2003 and above
benedicte
wxWorld Domination!
wxWorld Domination!
Posts: 1409
Joined: Wed Jan 19, 2005 3:44 pm
Location: Paris, France

Post by benedicte »

How do they react with WinXP themes ?
arucard
Experienced Solver
Experienced Solver
Posts: 61
Joined: Tue Dec 28, 2004 10:16 am
Location: Czech rep.

Post by arucard »

It's a generic control so the shape remains the same but the used font, height of tabs and colours may vary according to the actual windows settings. For example the result of the windows classic settings is that the background colour will be a bit more dark
wxWidgets 2.8.4, winxp, VC2003 and above
User avatar
ABX
Can't get richer than this
Can't get richer than this
Posts: 810
Joined: Mon Sep 06, 2004 1:43 pm
Location: Poznan, Poland
Contact:

Post by ABX »

arucard wrote:
if you post some code of using your class - it will be good ))
Ok, I do but there is not much to tell about.
To present book controls it is best to provide diff to notebook sample which adds new book control to the set of presented controls. This sample is written the way (common macros for wxNotebook, wxListbook and wxChoicebook) that it should be very easy.

ABX
CVS Head, 2.8.X
wxMSW, wxWinCE, wxPalmOS, wxOS2, wxMGL, bakefile
gcc 3.2.3, bcc 5.51, dmc 8.48, ow 1.6, vc 7.1, evc 3/4, pods 1.2
Chr
Earned some good credits
Earned some good credits
Posts: 115
Joined: Tue May 31, 2005 2:17 pm

Post by Chr »

yes - that would be great!
wxWidgets is nice
VonGodric
Earned some good credits
Earned some good credits
Posts: 103
Joined: Sun Jan 30, 2005 9:31 pm
Contact:

Post by VonGodric »

Looks nice :P
arucard
Experienced Solver
Experienced Solver
Posts: 61
Joined: Tue Dec 28, 2004 10:16 am
Location: Czech rep.

Post by arucard »

ABX wrote:To present book controls it is best to provide diff to notebook sample which adds new book control to the set of presented controls. This sample is written the way (common macros for wxNotebook, wxListbook and wxChoicebook) that it should be very easy.
ABX
It's a good idea but I don't think it would possible without hacking it. The reason is that this control doesn't contain any page windows so it isn't derived from wxBookCtrl with its AddPage(wxWindow *, ...) like other book controls in the sampe.
wxWidgets 2.8.4, winxp, VC2003 and above
Post Reply