custom gui elements - esp. non-rectangular gui elements

If you are a new Irrlicht Engine user, and have a newbie-question, this is the forum for you. You may also post general programming questions here.
Post Reply
CaptainPants
Posts: 19
Joined: Wed Dec 06, 2006 10:15 am

custom gui elements - esp. non-rectangular gui elements

Post by CaptainPants »

I was hoping someone could direct me to some good material on creating custom gui elements. I have been learning by looking through the basic tutorial, the API and the source for the built in elements, as well as the forum. I spent some time searching through the forum. I was wondering if anyone knows of a more helpful resource for beginners.

Some questions--
- Is there any documentation about what the return value of OnEvent is used for/ should signify? (API says: returns true if event was processed) What the environment does when false is returned ?
- Does anyone know of any custom gui elements I can get source for that use non-rectangular shapes?


I particularly want to create a diamond shaped button and a circular shaped button for a project I am attempting. I am having trouble with handling situations where two non-rectangular buttons overlap on the edges. The diamond shaped buttons will be tessellated.

Any help would be much appreciated
~~Captain Pants
arras
Posts: 1622
Joined: Mon Apr 05, 2004 8:35 am
Location: Slovakia
Contact:

Post by arras »

Hi, I was playing a little bit with custom gui. Bellow is my cusstom button class code:

Code: Select all

class MyButton : public gui::IGUIElement
{
    video::ITexture *image;
public:
    
    MyButton(gui::IGUIEnvironment* environment, gui::IGUIElement* parent, s32 id, core::rect<s32> rectangle) 
        : IGUIElement(gui::EGUIET_ELEMENT , environment, parent, id, rectangle)
    {
        image = NULL;
    }
    
    ~MyButton(){}
    
    virtual void setImage(video::ITexture *img)
    {
        image = img;
    }
    
    virtual void draw()
    {
        if (!IsVisible)
		return;
		
		video::IVideoDriver* driver = Environment->getVideoDriver();
		
		if(image)
		{
            core::rect<s32> rect = core::rect<s32>(0,0, image->getSize().Width, image->getSize().Height);
            core::rect<s32> pos = AbsoluteRect;
            driver->draw2DImage(image, pos, rect, 0, 0, true);
        }
    }
    
    bool OnEvent(SEvent event)
    {
        if (!IsEnabled)
            return Parent ? Parent->OnEvent(event) : false;
        
        switch(event.EventType)
        {
            case EET_MOUSE_INPUT_EVENT:
                if (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN)
                {
                    core::rect<s32> pos = AbsoluteRect;
                    if(pos.isPointInside(core::position2d<s32>(event.MouseInput.X, event.MouseInput.Y)) ) cout << "button clicked" << endl;
                    Environment->setFocus(this);
                    return true;
                }
        }
        
        return Parent ? Parent->OnEvent(event) : false;
    }
};
and you can create button like:

Code: Select all

video::IImage *image; 
    image = driver->createImageFromFile("soldier.bmp");
    video::ITexture *texture = driver->addTexture("texture", image);
    image->drop();
    
	MyButton *button = new MyButton( guienv, guienv->getRootGUIElement(), -1, core::rect<s32>(0,0,100,100) );
	button->drop();
	button->setImage(texture);
As you can see in OnEvent function, it simply detect if mouse cursor is inside button rectangle when mouse is clicked and if yes, print "button clicked" in the console. Button is rectangular but it would be quit easy to make it circular. Just keep position of center stored somewhere and instead of testing cursor against rectangle, test its distance from center of button. If it is it smaller than radius of your button, button was clicked.

In fact you dont have to store button center position, with some math, you can get it out of AbsoluteRect together with radius. (note that AbsoluteRect is inherited from IGUIElement).

As for diamond shaped elements I dont have idea. You may split your button in to smaller rectangles. Sort of pixelate your button. In such a way ou can build any shape you wish.
CaptainPants
Posts: 19
Joined: Wed Dec 06, 2006 10:15 am

Post by CaptainPants »

I refrained from posting my code earlier because of its length, but here is some relevant code:

[CGUIButton2.h]

Code: Select all

#ifndef CGUIBUTTON2_H_INCLUDED
#define CGUIBUTTON2_H_INCLUDED

#include <irr/irrlicht.h>
using namespace irr;
using namespace core;
using namespace gui;
using namespace video;

class CGUIButton2 : public IGUIElement
{
public:
    CGUIButton2
       (IGUIEnvironment * const & gui_env,
        const rect<s32> & shape,
        const s32 & id = -1,
        IGUIElement * const & parent = NULL)
        :   IGUIElement
               (EGUIET_ELEMENT,
                gui_env,
                parent ? parent : gui_env->getRootGUIElement(),
                id,
                shape),
            m_down (false),
            m_hover(false)
    {
    }

    virtual bool OnEvent(SEvent event)
    {
        if(event.EventType == EET_MOUSE_INPUT_EVENT)
        {
            position2d<s32> mouse_pos = position2d<s32>(event.MouseInput.X, event.MouseInput.Y);

            switch(event.MouseInput.Event)
            {
            case EMIE_LMOUSE_PRESSED_DOWN:
                if(m_hover)
                {
                    m_down = true;
                    Environment->setFocus(this);
                }
                else
                {
                    Environment->removeFocus(this);
                }
                break;

            case EMIE_LMOUSE_LEFT_UP:
                if(m_hover && m_down)
                {
                    SEvent new_e;
                    new_e.EventType          = EET_GUI_EVENT;
                    new_e.GUIEvent.EventType = EGET_BUTTON_CLICKED;
                    new_e.GUIEvent.Caller    = this;
                    Parent->OnEvent(new_e);
                }

                m_down = false;
                Environment->removeFocus(this);
                break;

            case EMIE_MOUSE_MOVED:
                m_hover = contains(mouse_pos);
                break;

            default:
                break;
            }
        }
        else if(event.EventType == EET_GUI_EVENT)
        {
            if( event.GUIEvent.Caller == dynamic_cast<IGUIElement *>(this) )
            {
                switch(event.GUIEvent.EventType)
                {
                case EGET_ELEMENT_LEFT:
                    m_hover = false;
                    break;

                case EGET_ELEMENT_HOVERED:
                    if(!m_hover)
                    {
                        return false;
                    }
                    break;

                default:
                    break;
                }
            }
        }

        return IGUIElement::OnEvent(event);
    }

    virtual void draw()
    {
        // draws a diamond
        position2d<s32> center = AbsoluteRect.getCenter();
        position2d<s32> top   (center.X, AbsoluteRect.UpperLeftCorner.Y   );
        position2d<s32> bottom(center.X, AbsoluteRect.LowerRightCorner.Y-1);
        position2d<s32> left  (AbsoluteRect.UpperLeftCorner.X,    center.Y);
        position2d<s32> right (AbsoluteRect.LowerRightCorner.X-1, center.Y);

        SColor col = m_hover ? SColor(255, 255, 255, 255) : SColor(255, 0, 0, 0);

        IVideoDriver * driver = Environment->getVideoDriver();
        driver->draw2DLine(left,   top,    col);
        driver->draw2DLine(top,    right,  col);
        driver->draw2DLine(right,  bottom, col);
        driver->draw2DLine(bottom, left,   col);
    }

protected:
    virtual bool contains(const position2d<s32> & position) const
    {
        position2d<s32> relative_pos(position - AbsoluteRect.getCenter());

        return
            AbsoluteClippingRect.isPointInside(position) &&
            abs_( (float)relative_pos.X / RelativeRect.getWidth() ) + abs_( (float)relative_pos.Y / RelativeRect.getHeight() ) < 0.5;
    }

    bool m_down,
         m_hover;
};
#endif // CGUIBUTTON2_H_INCLUDED

This is just a demonstration of the problem that I have no idea how to solve:
[main.cpp]

Code: Select all

#include <irr/irrlicht.h>
using namespace irr;
using namespace core;
using namespace gui;
using namespace video;

#include "CGUIButton2.h"

#include <iostream>

class CReceiver : public IEventReceiver
{
public:
    virtual bool OnEvent(SEvent event)
    {
        if(event.EventType == EET_GUI_EVENT)
        {
            if(event.GUIEvent.EventType == EGET_BUTTON_CLICKED)
            {
                std::cout << event.GUIEvent.Caller->getID() << std::endl;
                return true;
            }
        }

        return false;
    }
};

int main()
{
    IrrlichtDevice *device =
		createDevice(EDT_SOFTWARE, dimension2d<s32>(512, 384), 16,
			false, false, false, 0);

    IGUIEnvironment * gui_env = device->getGUIEnvironment();
    IVideoDriver    * driver  = device->getVideoDriver();

    (new CGUIButton2(gui_env, rect<s32>(100, 100, 199, 199), 0, NULL))->drop();
    (new CGUIButton2(gui_env, rect<s32>(200, 100, 299, 199), 1, NULL))->drop();
    (new CGUIButton2(gui_env, rect<s32>(150, 150, 249, 249), 2, NULL))->drop();

    CReceiver rec;
    device->setEventReceiver(&rec);

    while(device->run())
    {
        driver->beginScene(true, true, SColor(0, 122, 65, 171));

        gui_env->drawAll();

        driver->endScene();
        device->yield();
    }

    device->drop();

    return 0;
}
In the above example, everything works except that when the invisible corner of one button is over the top of another, it receives the gui events for the mouse when its in that position.
(put the mouse cursor over the bottom right corner of the left diamond, it doesnt light up)
~~Captain Pants
bitplane
Admin
Posts: 3204
Joined: Mon Mar 28, 2005 3:45 am
Location: England
Contact:

Post by bitplane »

- Is there any documentation about what the return value of OnEvent is used for/ should signify? (API says: returns true if event was processed) What the environment does when false is returned ?
you return true if your element absorbed the event, otherwise return Parent->OnEvent if you have a parent, or false if not.
for example, if you have a scrollbar inside an edit-box, you focus the scrollbar's button and then press the "x" key, the key isn't processed by the button so it is passed to the scrollbar (returns Parent->OnEvent), which also doesn't process it, so it is passed to the editbox which inserts the character x (and returns true).
- Does anyone know of any custom gui elements I can get source for that use non-rectangular shapes?
The problem with non-rectangular elements is that getElementFromPoint is called recursively to identify which element is clicked. unfortunately this method is not virtual, so you can't override it in your custom elements.
I guess that the element it's self shouldn't really be concerned with it's shape, the skin should worry about this instead.
If anyone has suggestions on how to resolve this I'm all ears.
Submit bugs/patches to the tracker!
Need help right now? Visit the chat room
CuteAlien
Admin
Posts: 9734
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Post by CuteAlien »

What speaks against making getElementFromPoint virtual? I did that here.

Edit: Thinking some more about it, I see that a better solution would to add a virtual method isPointInside to IGUIElement and using that instead of AbsoluteClippingRect.isPointInside(point)

This way it also wouldn't change the existing API but just add something.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
bitplane
Admin
Posts: 3204
Joined: Mon Mar 28, 2005 3:45 am
Location: England
Contact:

Post by bitplane »

good idea, i'll go with that :)

as we discussed on irc, I'll also add a way for the skin to calculate whether a point is within the element's area.. but that's another change completely, so I'll leave it until after the tab stuff is working
Submit bugs/patches to the tracker!
Need help right now? Visit the chat room
CaptainPants
Posts: 19
Joined: Wed Dec 06, 2006 10:15 am

Post by CaptainPants »

Thanks guys, I personally went with changing the engine slightly to put this into IGUIElement. :) I can now override this for non-rectangular buttons.

Code: Select all

	virtual bool isPointInside(const core::position2d<s32> & point) const
	{
		return AbsoluteClippingRect.isPointInside(point);
	}
A further question if anyones interested:
Why exactly is the argument to OnEvent passed by value and not by a const reference (or even non-const)? seeing as it is potentially copied a large number of times on its way up the tree, and it has several data members. (I imagine this is partly because it was done like this originally and noone wanted to change it and cause all gui elements to require the slight change)
Last edited by CaptainPants on Fri Jul 06, 2007 1:05 pm, edited 2 times in total.
~~Captain Pants
vitek
Bug Slayer
Posts: 3919
Joined: Mon Jan 16, 2006 10:52 am
Location: Corvallis, OR

Post by vitek »

Yeah, hybrid changed it to a const reference for a while back about version 0.14, but changed it back again. I'm pretty sure it was reverted because it broke source compatibility... i.e. users would have to modify and recompile their code to get it to work with the new signature. Unfortunately, if it was done for compatibility reasons, it will have to wait because they just released version 1.3.1 and changes that break source compatibility usually are reserved for minor or major version releases.

Travis
hybrid
Admin
Posts: 14143
Joined: Wed Apr 19, 2006 9:20 pm
Location: Oldenburg(Oldb), Germany
Contact:

Post by hybrid »

We'll add this const-ref back when the new animation system is added as that will break all sources anyway.
Post Reply