(C++) Scrolling Tab Control v1.4

Post those lines of code you feel like sharing or find what you require for your project here; or simply use them as tutorials.
Post Reply
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

(C++) Scrolling Tab Control v1.4

Post by JP »

So the standard Irrlicht tab control class doesn't handle tabs very well, if you add too many then they just disappear off the edge of the control, so i've implemented a scrolling version so you can see all tabs that have been added.

I think someone else made one of these but for some reason i couldn't use it, can't remember why as it was quite a while ago!

I've updated it since the first version, here's the list of features now:
- Scroll left and right through the tabs
- Limit the maximum width of a tab
- If the tab's name is too large for this maximum width it's reduced and has "..." on the end
- When this tab is selected it expands to its full size

Here's a download link with a compiled binary and the source:
http://www.mediafire.com/?nizhynzkzj2

It works with Irrlicht 1.4.2

In the following post i'll give the entire source in case the download link dies.

Use this code as you wish, you don't need to mention my name at all as it's a pretty small bit of code that i've added.

If you've got any suggestions for improvement feel free to let me know (by PM is probably best as i don't check the code snippets forum much).

Here's some screenshots:

Tab 1 selected, showing tab 2, with its long name clipped:
Image

Tab 2 selected, showing its long name unclipped:
Image

Tab 7 selected, showing the tabs scrolled to show that selection:
Image
Last edited by JP on Thu Sep 25, 2008 10:37 am, edited 7 times in total.
Image Image Image
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

You may notice it's a complete copy of CGUITab and CGUITabControl with some added functionality for CGUIScrollingTabControl.

CGUIScrollingTabControl.h

Code: Select all

#ifndef INC_CGUISCROLLINGTABCONTROL_H
#define INC_CGUISCROLLINGTABCONTROL_H

#include <irrlicht.h>

using namespace irr;
using namespace gui;

    // A tab, onto which other gui elements could be added.
	class CGUITab : public IGUITab
	{
	public:

		//! constructor
		CGUITab(s32 number, IGUIEnvironment* environment,
			IGUIElement* parent, const core::rect<s32>& rectangle,
			s32 id);

		//! Returns number of this tab in tabcontrol. Can be accessed
		//! later IGUITabControl::getTab() by this number.
		virtual s32 getNumber() const;

		//! Sets the number
		virtual void setNumber(s32 n);

		//! draws the element and its children
		virtual void draw();

		//! sets if the tab should draw its background
		virtual void setDrawBackground(bool draw=true);

		//! sets the color of the background, if it should be drawn.
		virtual void setBackgroundColor(video::SColor c);

		//! returns true if the tab is drawing its background, false if not
		virtual bool isDrawingBackground() const;

		//! returns the color of the background
		virtual video::SColor getBackgroundColor() const;

		//! Writes attributes of the element.
		virtual void serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options) const;

		//! Reads attributes of the element
		virtual void deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options);


	private:

		s32 Number;
		bool DrawBackground;
		video::SColor BackColor;
	};


	//! A standard tab control
	class CGUIScrollingTabControl : public IGUITabControl
	{
	public:

		//! destructor
		CGUIScrollingTabControl(IGUIEnvironment* environment,
			IGUIElement* parent, const core::rect<s32>& rectangle,
			bool fillbackground=true, bool border=true, s32 id=-1);

		//! destructor
		virtual ~CGUIScrollingTabControl();

		//! Adds a tab
		virtual CGUITab* addTab(const wchar_t* caption, s32 id=-1);

		//! Adds a tab that has already been created
		virtual void addTab(CGUITab* tab);

		//! Returns amount of tabs in the tabcontrol
		virtual s32 getTabCount() const;

		//! Returns a tab based on zero based index
		virtual IGUITab* getTab(s32 idx) const;

		//! Brings a tab to front.
		virtual bool setActiveTab(s32 idx);

		//! Brings a tab to front.
		virtual bool setActiveTab(IGUIElement *tab);

		//! Returns which tab is currently active
		virtual s32 getActiveTab() const;

		//! called if an event happened.
		virtual bool OnEvent(const SEvent& event);

		//! draws the element and its children
		virtual void draw();

		//! Removes a child.
		virtual void removeChild(IGUIElement* child);

		//! Writes attributes of the element.
		virtual void serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options) const;

		//! Reads attributes of the element
		virtual void deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options);
		
		//! Returns the maximum width of a tab
		virtual inline u32 getMaxTabWidth() { return MaxTabWidth; }
		
		//! Sets the maximum width of a tab
		virtual inline void setMaxTabWidth(u32 val) { MaxTabWidth = val; }

	private:

        s32 getTabsLengthUpTo(s32 tab);
        s32 getActiveTabLength();
		void selectTab(core::position2d<s32> p);

		core::array<CGUITab*> Tabs;
		s32 ActiveTab;
		s32 TabStartPosX;
		u32 MaxTabWidth;
		bool Border;
		bool FillBackground;
		IGUIButton* leftButton;
        IGUIButton* rightButton;
	};

#endif /* INC_CGUISCROLLINGTABCONTROL_H */
CGUIScrollingTabControl.cpp

Code: Select all

#include "CGUIScrollingTabControl.h"

//! constructor
CGUITab::CGUITab(s32 number, IGUIEnvironment* environment,
	IGUIElement* parent, const core::rect<s32>& rectangle, 
	s32 id)
	: IGUITab(environment, parent, id, rectangle), Number(number),
		DrawBackground(false), BackColor(0,0,0,0)
{
	#ifdef _DEBUG
	setDebugName("CGUITab");
	#endif
}


//! Returns number of tab in tabcontrol. Can be accessed
//! later IGUITabControl::getTab() by this number.
s32 CGUITab::getNumber() const
{
	return Number;
}


//! Sets the number
void CGUITab::setNumber(s32 n)
{
	Number = n;
}


//! draws the element and its children
void CGUITab::draw()
{
	if (!IsVisible)
		return;

	IGUISkin *skin = Environment->getSkin();

	if (skin && DrawBackground)
		skin->draw2DRectangle(this, BackColor, AbsoluteRect, &AbsoluteClippingRect);

	IGUIElement::draw();
}


//! sets if the tab should draw its background
void CGUITab::setDrawBackground(bool draw)
{
	DrawBackground = draw;
}


//! sets the color of the background, if it should be drawn.
void CGUITab::setBackgroundColor(video::SColor c)
{
	BackColor = c;
}


//! returns true if the tab is drawing its background, false if not
bool CGUITab::isDrawingBackground() const
{
	_IRR_IMPLEMENT_MANAGED_MARSHALLING_BUGFIX;
	return DrawBackground;
}


//! returns the color of the background
video::SColor CGUITab::getBackgroundColor() const
{
	return BackColor;
}


//! Writes attributes of the element.
void CGUITab::serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options=0) const
{
	IGUITab::serializeAttributes(out,options);

	out->addInt	("TabNumber",		Number);
	out->addBool	("DrawBackground",	DrawBackground);
	out->addColor	("BackColor",		BackColor);

}


//! Reads attributes of the element
void CGUITab::deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options=0)
{
	IGUITab::deserializeAttributes(in,options);

	setNumber(in->getAttributeAsInt("TabNumber"));
	setDrawBackground(in->getAttributeAsBool("DrawBackground"));
	setBackgroundColor(in->getAttributeAsColor("BackColor"));

	if (Parent && Parent->getType() == EGUIET_TAB_CONTROL)
	{
		((CGUIScrollingTabControl*)Parent)->addTab(this);
		if (isVisible())
			((CGUIScrollingTabControl*)Parent)->setActiveTab(this);
	}
}






//! constructor
CGUIScrollingTabControl::CGUIScrollingTabControl(IGUIEnvironment* environment,
	IGUIElement* parent, const core::rect<s32>& rectangle, 
	bool fillbackground, bool border, s32 id)
	: IGUITabControl(environment, parent, id, rectangle), ActiveTab(-1),
	Border(border), FillBackground(fillbackground)
{
	#ifdef _DEBUG
	setDebugName("CGUIScrollingTabControl");
	#endif
	
	TabStartPosX = AbsoluteRect.UpperLeftCorner.X + 25;
	MaxTabWidth = AbsoluteRect.getWidth()/2;
	leftButton = environment->addButton(core::rect<s32>(0, 0, 20, 30), this, -1, L"<");
    rightButton = environment->addButton(core::rect<s32>(rectangle.getWidth()-20, 0, rectangle.getWidth(), 30), this, -1, L">");
}


//! destructor
CGUIScrollingTabControl::~CGUIScrollingTabControl()
{
	for (u32 i=0; i<Tabs.size(); ++i)
	{
		if (Tabs[i])
			Tabs[i]->drop();
	}
}


//! Adds a tab
CGUITab* CGUIScrollingTabControl::addTab(const wchar_t* caption, s32 id)
{
	IGUISkin* skin = Environment->getSkin();
	if (!skin)
		return 0;

	s32 tabheight = skin->getSize(gui::EGDS_BUTTON_HEIGHT) + 2;
	core::rect<s32> r(1,tabheight,
		AbsoluteRect.getWidth()-1,
		AbsoluteRect.getHeight()-1);

	CGUITab* tab = new CGUITab(Tabs.size(), Environment, this, r, id);
	tab->setAlignment(EGUIA_UPPERLEFT, EGUIA_LOWERRIGHT, EGUIA_UPPERLEFT, EGUIA_LOWERRIGHT);

	tab->setText(caption);
	tab->setVisible(false);
	Tabs.push_back(tab);

	if (ActiveTab == -1)
	{
		ActiveTab = 0;
		tab->setVisible(true);
	}

	return tab;
}


//! adds a tab which has been created elsewhere
void CGUIScrollingTabControl::addTab(CGUITab* tab)
{
	if (!tab)
		return;

	// check if its already added
	for (u32 i=0; i < Tabs.size(); ++i)
	{
		if (Tabs[i] == tab)
			return;
	}

	tab->grab();

	if (tab->getNumber() == -1)
		tab->setNumber((s32)Tabs.size());

	while (tab->getNumber() >= (s32)Tabs.size())
		Tabs.push_back(0);

	if (Tabs[tab->getNumber()])
	{
		Tabs.push_back(Tabs[tab->getNumber()]);
		Tabs[Tabs.size()-1]->setNumber(Tabs.size());
	}
	Tabs[tab->getNumber()] = tab;

	if (ActiveTab == -1)
		ActiveTab = tab->getNumber();


	if (tab->getNumber() == ActiveTab)
	{
		setActiveTab(ActiveTab);
	}
}


//! Returns amount of tabs in the tabcontrol
s32 CGUIScrollingTabControl::getTabCount() const
{
	return Tabs.size();
}


//! Returns a tab based on zero based index
IGUITab* CGUIScrollingTabControl::getTab(s32 idx) const
{
	if ((u32)idx >= Tabs.size())
		return 0;

	return Tabs[idx];
}


//! called if an event happened.
bool CGUIScrollingTabControl::OnEvent(const SEvent& event) {
     
	if (!IsEnabled)
		return Parent ? Parent->OnEvent(event) : false;

	switch(event.EventType) {
      case EET_GUI_EVENT: {
        switch (event.GUIEvent.EventType) {
          case EGET_BUTTON_CLICKED:
            if (event.GUIEvent.Caller == leftButton) {
              s32 idx = ActiveTab - 1;
              if (idx < 0) idx = 0; 
              setActiveTab(idx); 
              return true;                      
            } else if (event.GUIEvent.Caller == rightButton) {
              s32 idx = ActiveTab + 1;
              if (idx > Tabs.size()-1) idx = Tabs.size()-1;   
              setActiveTab(idx);
              return true;  
            }
        }
        break;
      }
	  case EET_MOUSE_INPUT_EVENT: 
		switch(event.MouseInput.Event) {
		  case EMIE_LMOUSE_PRESSED_DOWN:
			Environment->setFocus(this);
			return true;
	  	  case EMIE_LMOUSE_LEFT_UP:
			Environment->removeFocus(this);
			s32 offset = TabStartPosX - (AbsoluteRect.UpperLeftCorner.X + 25); // offset due to scrolling
			selectTab(core::position2d<s32>(event.MouseInput.X - offset, event.MouseInput.Y));
			return true;
		}
		break;
	}

	return Parent ? Parent->OnEvent(event) : false;
}


void CGUIScrollingTabControl::selectTab(core::position2d<s32> p)
{
	IGUISkin* skin = Environment->getSkin();
	IGUIFont* font = skin->getFont();

	core::rect<s32> frameRect(AbsoluteRect);

	s32 tabheight = skin->getSize(gui::EGDS_BUTTON_HEIGHT);
	frameRect.UpperLeftCorner.Y += 2;
	frameRect.LowerRightCorner.Y = frameRect.UpperLeftCorner.Y + tabheight;
	s32 pos = frameRect.UpperLeftCorner.X + 2 + 25;

	for (u32 i=0; i<Tabs.size(); ++i)
	{
		// get Text
		const wchar_t* text = 0;
		if (Tabs[i])
			text = Tabs[i]->getText();

		// get text length
		s32 len = 0;
		if (font)
			len = font->getDimension(text).Width + 10;
		if (len > MaxTabWidth && (s32)i != ActiveTab) len = MaxTabWidth;

		frameRect.UpperLeftCorner.X = pos;
		frameRect.LowerRightCorner.X = frameRect.UpperLeftCorner.X + len;
		pos += len;

		if (frameRect.isPointInside(p))
		{
			setActiveTab(i);
			return;
		}
	}
}

s32 CGUIScrollingTabControl::getTabsLengthUpTo(s32 tab) {

  IGUIFont* font = Environment->getSkin()->getFont();
  const wchar_t* text = 0;
  s32 totalLength = 0;
  s32 len = 0;
  
  for (u32 i = 0 ; i < tab ; ++i) {
    if (Tabs[i]) {
      text = Tabs[i]->getText();
      len = font->getDimension(text).Width + 10;
      if (len > MaxTabWidth && (s32)i != ActiveTab) len = MaxTabWidth;
      totalLength += len;
    }
  }
 
  return totalLength;
     
}

s32 CGUIScrollingTabControl::getActiveTabLength() {

  IGUIFont* font = Environment->getSkin()->getFont();
  
  if (Tabs[ActiveTab]) return font->getDimension(Tabs[ActiveTab]->getText()).Width + 10;
  
  return 0;
      
}

//! draws the element and its children
void CGUIScrollingTabControl::draw()
{
	if (!IsVisible)
		return;

	IGUISkin* skin = Environment->getSkin();
	if (!skin)
		return;

	IGUIFont* font = skin->getFont();

	core::rect<s32> frameRect(AbsoluteRect);

	if (Tabs.empty())
		skin->draw2DRectangle(this, skin->getColor(EGDC_3D_HIGH_LIGHT),
		frameRect, &AbsoluteClippingRect);

	if (!font)
		return;

	s32 tabheight = skin->getSize(gui::EGDS_BUTTON_HEIGHT);
	frameRect.UpperLeftCorner.Y += 2;
	frameRect.LowerRightCorner.Y = frameRect.UpperLeftCorner.Y + tabheight;
	core::rect<s32> tr;
	// Check whether we need to shift the tabs backwards to make the selected one visible
	s32 prevLen = getTabsLengthUpTo(ActiveTab);
	s32 activeLen = getActiveTabLength();
	s32 offset = frameRect.UpperLeftCorner.X + 25;

	if (prevLen > ((frameRect.LowerRightCorner.X-frameRect.UpperLeftCorner.X)-50)-activeLen) offset -= activeLen + (prevLen - ((frameRect.LowerRightCorner.X-frameRect.UpperLeftCorner.X)-50));

    if (TabStartPosX != offset) TabStartPosX += (offset>TabStartPosX?1:-1);
    
	// left and right pos of the active tab
	s32 left = 0;
	s32 right = 0;
	core::stringw activetext = "";
	
	s32 pos = TabStartPosX;
	
	for (u32 i=0; i<Tabs.size(); ++i)
	{
		// get Text
		core::stringw text = "";
		if (Tabs[i])
			text = Tabs[i]->getText();

		// get text length
		s32 len = font->getDimension(text.c_str()).Width + 10;
		if (len > MaxTabWidth && (s32)i != ActiveTab) len = MaxTabWidth;
		frameRect.UpperLeftCorner.X = pos;
		frameRect.LowerRightCorner.X = frameRect.UpperLeftCorner.X + len;
		pos += len;

		if ((s32)i == ActiveTab)
		{
			left = frameRect.UpperLeftCorner.X;
			right = frameRect.LowerRightCorner.X;
			activetext = text;
		}
		else
		{
			skin->draw3DTabButton(this, false, frameRect, &AbsoluteClippingRect);
			
			if (len >= MaxTabWidth) {
              do {
                text.erase(text.size()-1);
              } while (font->getDimension(text.c_str()).Width + 10 > MaxTabWidth - 5);
              text += "...";
            }
			// draw text
			font->draw(text.c_str(), frameRect, skin->getColor(EGDC_BUTTON_TEXT),
				true, true, &AbsoluteClippingRect);
		}
	}

	// draw active tab
	if (left != 0 && right != 0)
	{
		frameRect.UpperLeftCorner.X = left-2;
		frameRect.LowerRightCorner.X = right+2;
		frameRect.UpperLeftCorner.Y -= 2;

		skin->draw3DTabButton(this, true, frameRect, &AbsoluteClippingRect);
		
		// draw text
		font->draw(activetext.c_str(), frameRect, skin->getColor(EGDC_BUTTON_TEXT),
			true, true, &AbsoluteClippingRect);

		// draw upper highlight frame
		tr.UpperLeftCorner.X = AbsoluteRect.UpperLeftCorner.X;
		tr.LowerRightCorner.X = left - 1;
		tr.UpperLeftCorner.Y = frameRect.LowerRightCorner.Y - 1;
		tr.LowerRightCorner.Y = frameRect.LowerRightCorner.Y;
		skin->draw2DRectangle(this, skin->getColor(EGDC_3D_HIGH_LIGHT), tr, &AbsoluteClippingRect);

		tr.UpperLeftCorner.X = right;
		tr.LowerRightCorner.X = AbsoluteRect.LowerRightCorner.X;
		skin->draw2DRectangle(this, skin->getColor(EGDC_3D_HIGH_LIGHT), tr, &AbsoluteClippingRect);
	}

	skin->draw3DTabBody(this, Border, FillBackground, AbsoluteRect, &AbsoluteClippingRect);

	IGUIElement::draw();
}


//! Returns which tab is currently active
s32 CGUIScrollingTabControl::getActiveTab() const
{
	return ActiveTab;
}


bool CGUIScrollingTabControl::setActiveTab(IGUIElement *tab)
{
	for (s32 i=0; i<(s32)Tabs.size(); ++i)
		if (Tabs[i] == tab)
			return setActiveTab(i);
	return false;
}


//! Brings a tab to front.
bool CGUIScrollingTabControl::setActiveTab(s32 idx)
{
	if ((u32)idx >= Tabs.size())
		return false;

	bool changed = (ActiveTab != idx);

	ActiveTab = idx;

	for (s32 i=0; i<(s32)Tabs.size(); ++i)
		if (Tabs[i])
			Tabs[i]->setVisible( i == ActiveTab );

	if (changed)
	{
		SEvent event;
		event.EventType = EET_GUI_EVENT;
		event.GUIEvent.Caller = this;
		event.GUIEvent.Element = 0;
		event.GUIEvent.EventType = EGET_TAB_CHANGED;
		Parent->OnEvent(event);		
	}

	return true;
}


//! Removes a child.
void CGUIScrollingTabControl::removeChild(IGUIElement* child)
{
	bool isTab = false;

	u32 i=0;
	// check if it is a tab
	while (i<Tabs.size())
	{
		if (Tabs[i] == child)
		{
			Tabs[i]->drop();
			Tabs.erase(i);
			isTab = true;
		}
		else
			++i;
	}

	// reassign numbers
	if (isTab)
	{
		for (i=0; i<Tabs.size(); ++i)
			if (Tabs[i])
				Tabs[i]->setNumber(i);
	}

	// remove real element
	IGUIElement::removeChild(child);
}


//! Writes attributes of the element.
void CGUIScrollingTabControl::serializeAttributes(io::IAttributes* out, io::SAttributeReadWriteOptions* options=0) const
{
	IGUITabControl::serializeAttributes(out,options);

	out->addInt("ActiveTab",	ActiveTab);
	out->addBool("Border",		Border);
	out->addBool("FillBackground",	FillBackground);
}


//! Reads attributes of the element
void CGUIScrollingTabControl::deserializeAttributes(io::IAttributes* in, io::SAttributeReadWriteOptions* options=0)
{
	Border		= in->getAttributeAsBool("Border");
	FillBackground  = in->getAttributeAsBool("FillBackground");

	ActiveTab = -1;

	IGUITabControl::deserializeAttributes(in,options);

	setActiveTab(in->getAttributeAsInt("ActiveTab"));
}
Usage is as simple as:

Code: Select all

IGUITabControl* tabControl = new CGUIScrollingTabControl(env, wnd, core::rect<s32>(6,25,294,193), true, true);
And it then behaves just like a normal IGUITabControl.
Last edited by JP on Thu Sep 25, 2008 10:34 am, edited 3 times in total.
Image Image Image
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

Updated to v1.1, see first post for more details.
Image Image Image
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

I've just noticed a couple of issues with the code, the scrolling doesn't work properly in another of my projects and any initial tabs scroll in from 'offscreen' at when you initially create it so i'll try and stop that happening and update this post.
Image Image Image
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

Updated to v1.2 again.

Now something i've thought is how to use the scroll buttons.. currently they select the previous/next tab but maybe they should just scroll the tabs along rather than changing the selection...
Image Image Image
stevend
Posts: 114
Joined: Sat Mar 01, 2008 7:18 pm

Post by stevend »

sweet, i will have to use this for my level editor

thanksha!
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

There will be at least one more addition so keep your eyes open and in fact a future version of irrlicht will certainly have some form of scrolling tab control in it which is great news!
Image Image Image
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

Ok final addition added, just wasn't using the right rectangle in the constructor basically!
Image Image Image
Halifax
Posts: 1424
Joined: Sun Apr 29, 2007 10:40 pm
Location: $9D95

Post by Halifax »

Great job JP. Keep up the good work.
TheQuestion = 2B || !2B
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

Updated again to fix two bugs; when you moved the window the tabs would scroll around needlessly and there was also a memory issue with the tab titles which would mean the correct title wasn't quite displayed.

Also compiled it with Irrlicht 1.4.2 now.
Image Image Image
Halan
Posts: 447
Joined: Tue Oct 04, 2005 8:17 pm
Location: Germany, Freak City
Contact:

Post by Halan »

nice!

what do we have to do to get it into irrlicht?
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

you don't get it into irrlicht, it's just a class that you add into your project and use as is shown in the demo app ;)
Image Image Image
fmx

Post by fmx »

or does he mean "integrate it into the irrlicht engine so its part of the core distribution in a future version"?
JP
Posts: 4526
Joined: Tue Sep 13, 2005 2:56 pm
Location: UK
Contact:

Post by JP »

Could be... well bitplane was all up for this, just sort of required me to match up the coding style to that of Irrlicht. I've actually got an improved version with Varmint's update and some of the coding style stuff done so i'll have to take another look and see if it's in good enough shape!
Image Image Image
Post Reply