Using Connect To Add Events To An Existing Class

From WxWiki

Jump to: navigation, search

This applies to wxWindows 2.4.2; wxWindows 2.6.3 provides an additional parameter to wxEvtHandler::Connect, wxEvtHandler* eventSink, that should be able to obtain the same results as below, but in a non-offensive way.

WARNING I am not a very advanced C++ programmer, so this example is probably a horrendously ugly hack that should not be used by anyone. I fervently hope someone with better knowledge of the language will annotate this with proper usage and explanations so I can learn how to do it the right way.

Contents

[edit] Background

I was trying to extend the wxCalendarCtrl class to handle right-click events (for example, to display a popup menu). The base class does not declare an event map for this event (EVT_RIGHT_UP), so it seemed like I needed to dig in and do it myself. Two solutions occured to me:

    1. Extend wxCalendarCtrl and add an EVT_RIGHT_UP call in the event table.
    2. Use wxEvtHandler::Connect to connect the event at runtime.

Since I'm wary of subclassing common controls and was feeling adventurous, I tried the second solution.

[edit] First Attempt

I could find no documentation, however, of using Connect() in this exact manner; that is, to add events to a base class. I only found examples of adding events to one's own child of wxFrame, like so:

// frame is of type MyFrame : wxFrame
frame->Connect( ID_Quit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MyFrame::OnQuit) );

I was looking for something like:

// m_cal is of type wxCalendarCtrl, not a subclass
// MyPanel extends wxPanel and has m_cal as a member variable
MyPanel::MyPanel()
{
    ... // initialization
    m_cal->Connect( Calendar_Ctrl, wxEVT_RIGHT_UP, wxCommandEventHandler(MyPanel::OnCalRightClick) );
}

and the handler itself:

// this is supposed to make right-clicking obtain the same effect as left-clicking
void MyPanel::OnCalRightClick(wxMouseEvent& event)
{
    wxDateTime newdate;

    wxPoint pos = event.GetPosition();
    wxCalendarHitTestResult res = m_cal->HitTest(pos, &newdate);
    
    if ( res == wxCAL_HITTEST_DAY)
        m_cal->SetDate(newdate);
}

Of course, this doesn't work - segmentation faults and access violations ahoy! - but the reason may be opaque; it was for me.

[edit] Diagnosing The Problem

Careful debugging revealed the problem: as written, MyPanel::OnCalRightClick assumes that *this is of type MyPanel; but it actually ended up being of type wxCalendarCtrl! So my experiment reveals a characteristic of wxEvtHandler::Connect that is not explicitly documented (though it may be obvious to those who actually know C++): the wxObjectEventFunction passed to Connect() will be called with this set to whatever called Connect().

In effect, OnCalRightClick is not a member of MyPanel; or, more precisely, it is declared a member of MyPanel and is usable by any class granted access to it, but it also can morph via Connect() to be a member of whatever wxEvtHandler-derived class we want. This is at once beautiful and hideous.

[edit] The Horrendous Solution

So the problem is that MyPanel is the declared context of OnCalRightClick() (which the compiler will hold us to), while its implicit context (thanks to our usage) is wxCalendarCtrl. Obviously, we can't just declare wxCalendarCtrl::OnCalRightClick(); it's not allowed, and it's what we wanted to avoid anyway. The other option is to cast this to wxCalendarCtrl.

MyPanel::MyPanel()
{
    ... // initialization
    m_cal->Connect( Calendar_Ctrl, wxEVT_RIGHT_UP, wxCommandEventHandler(MyPanel::OnCalRightClick) );
}
// this makes right-clicking obtain the same effect as left-clicking
void MyPanel::OnCalRightClick(wxMouseEvent& event)
{
    wxCalendarCtrl* cal = (wxCalendarCtrl*) this;
    wxDateTime newdate;

    wxPoint pos = event.GetPosition();
    wxCalendarHitTestResult res = cal->HitTest(pos, &newdate);
    
    if ( res == wxCAL_HITTEST_DAY)
        cal->SetDate(newdate);
}

This compiles as of wxWindows 2.4.2 under Microsoft's Visual C++ 6.0 and yields the desired behavior. It also (to my mind) totally violates principles of type safety, object encapsulation, and intuition.

Someone please correct this page and put it out of its misery.

[edit] The Requested Correction

I think you can solve this issue by passing "this" to the event sink parameter of Connect, rather than leaving it at the default value of NULL. -- Anon wx user

Personal tools