Anyway I don't know if this strange behaviour is by design for some reason or it is a kind of bug. May be I just do know where the right way lies.
I am trying to implement simple layout inside mainwindow frame. Picture (1) on attached image.
I want to get sidebar on the left with controls. It may contain lots of controls, and I want it to be vertically scrollable if needed (vscrollbar should appear when window is resized or when more controls added from program). Sidebar is of some fixed width or it can gain a little more space when mainwindow frame is too wide, "fixed width" should be calculated automatically by sizer or based on the auto-calculated value.
The main part of frame should contain canvas (of some size) with painting on the panel, panel has to be scrollable if canvas does not fit the viewport.
There are some macro-vars defined for the following source that allows to compile different versions of program.
1. If source is compiled as is, wxPanel is used as a sidebar holder, everything with sidebar and general layout is ok (but no scrollbars and no scrolling), sizer does its work as expected. Picture (2).
2. If source is compiled with -D USE_SCROLLED, wxScrolled<wxPanel> is used in place of simple wxPanel, but... nothing changes. No scrollbar appear even if I resize frame to minimum with a mouse.
3. If source is compiled with both -D USE_SCROLLED and -D USE_SCROLLRATE, calls to ->SetScrollRate(1, 1) are added to both sidebar wxScrolled-panel and mainview wxScrolled-panel. Voila, now scrollbars appear on resize - as needed, excellent. But... layout is broken. Picture (3).
Look, I asked for vertical scrollbar only in case of sidebar. So far I expect that in connection to horizontal sizing it would behave like an usual panel, like it was with simple not-scrollable wxPanel. I expect wxScrolled-panel should use certain minimal size of its child panel if that was set, or it should use "optimal" size of its child panel calculated by sizer and use that width to set one's own width to fit around child (horizontally). But instead wxScrolled assumes its child width is 0 or so... or maybe the width of vertical scrollbar is taken by wxScrolled as its minimal horizontal size.
4. Macro var USE_MINSIZE added. SetMinSize() calls added for:
- sidebar panel (not wxScrolled-panel but to its child, I expect scroller (not scrollable in this direction) must respect it, but it does not)
- to maincanvas wxScrolled-panel, I expect this would prevent viewport panel from minimizing to ugly mess (as on picture (5)).
Nothing changed, layout is still broken.
wxScrolled with sizer set completely ignores child widget's "best size" and min-size for its not-scrollable direction and does not fit around child unlike wxPanel does.
wxScrolled completely drops(?) min-size set to its viewport scrollable direction - it allows parent sizer to scale itself to zero. Min-client-size set to wxScrolled's not-scrollable direction works partially (see below).
5. Macro var USE_MINSIZE removed and USE_MINSIZE_2 added (-D USE_SCROLLED -D USE_SCROLLRATE -D USE_MINSIZE_2).
Now I try to transfer by hand the calculated by wx "best size.width" of scrollbar panel to its parent scroller client-min-size.
Almost good. Layout is ok. All scrollbars appear and disappear as expected. The only bad thing is that when sidebar's scrollbar appears, it eats some horizonal space and sidebar panel with controls does not fit its scroller area anymore, right edge of controls is hidden under scrollbar when scrollbar appears. Picture (4).
What do I expect? I expect that the wxScrolled, when vscrollbar has to appear and needs space, will "push" edge between sidebar and mainarea to right (well, actually it is sizer/layout manager who adjusts layout) to get space for vscrollbar.
I will try to imitate this behaviour programmatically, but it is not too easy and I'm afraid solution may be not too reliable. Unfortunately I do not see special events on scrollbar appear/disappear to use them as a signal to start recalculation of layout.
6. Macro USE_EVTHANDLER added (-D USE_SCROLLED -D USE_SCROLLRATE -D USE_MINSIZE_2 -D USE_EVTHANDLER).
So what the handleSizeEvent() code does? It does crazy thing. When it sees that wxScrolled failed to respect client area min-size the scroller was told to guarantee, event handler insist on this and tells wxScrolled to respect (quite the same) min-size once more. That fixes layout. Imperfectly, as size event is arised when layout had already become broken, so changes come in two steps: at first broken at second fixed.
What is possible else?
7. Macro var USE_MINSIZE_2 replaced with USE_MINSIZE_3 (-D USE_SCROLLED -D USE_SCROLLRATE -D USE_MINSIZE_3).
Now I reserve 20 pixel more space for scroller than child panel needs. Now there is empty space on right edge of panel and scrollbar takes it when it appears.
Or:
8. Macro var USE_MINSIZE_3A added (-D USE_SCROLLED -D USE_SCROLLRATE -D USE_MINSIZE_3 -D USE_MINSIZE_3A).
Additionally wxEXPAND is passed to scroller sizer, now sidebar panel takes 20 extra pixels initially and squeezes back when scrollbar appears but still fits inside scroller viewport.
So far I do not know a way how to implement (without handcrafted hooks like that insistent eventhandler) behaviour I mentioned above: when scrollbar's appearance makes wxScrolled to increment its outer size (in not-scrollable direction, instead of eating inner viewport space that promised to respect child's min-size requirement). But with some handcraft I at least can get satisfying results.
But I expected it to work out of the box: replace wxPanel with wxScrolled<wxPanel>, add it to the parent window's sizer, set own sizer for wxScrolled object and that's it. Unfortunately not true.
By the way I want to say there is one more topic I would be glad to find more information upon. It is handling of tab, shift+tab, up/down arrow keys by wxPanel. AFAIK in Java Swing and in Javascript browser DOM systems key/mouse input events do complex path there: from topwindow root they go up to target widget and then go down back to topwindow if not "consumed" on some stage, so far every parent window can intercept and handle incoming events before they reach target child widget. Wx docs say events start their trip from target widget (well, after optional filtering on app-level), so parent widget have no possibility to intercept... so far how is tab/shift+tab/up/down handling implemented? I suspect wxControl-family widgets "by contract" prefilter and propagate special key events to parent widget... or do not do it in case control was constructed with wxWANTS_CHARS? and call to .Navigate() may be used by programmer in subclasses to propagate special key events to parent panel if he wants to?
It would be great if someone is so kind to shed some light of knowledge into this fog of mistery. Unfortunately events overview from doc keeps this in secret, API doc keeps underlying class undocumented by intention.
Code: Select all
#include <iostream>
#include <wx/app.h>
#include <wx/window.h>
#include <wx/frame.h>
#include <wx/event.h>
#include <wx/panel.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/sizer.h>
#include <wx/scrolwin.h>
#include <wx/stattext.h>
// #include <wx/.h>
// #include <wx/.h>
class MyApp : public wxApp
{
public:
MyApp(void) ;
virtual bool OnInit(void) ;
} ;
class MainWindow : public wxFrame {
public:
explicit MainWindow(void) ;
#ifdef USE_EVTHANDLER
void handleSizeEvent(wxEvent&) ;
#endif
protected:
wxWindow* buildSideBar(wxWindow* p_parentwnd) ;
private:
#ifdef USE_EVTHANDLER
wxWindow* _p_scroller ;
wxWindow* _p_sidebar ;
int _minsize ;
#endif
} ;
wxIMPLEMENT_APP(MyApp) ;
MyApp::MyApp(void)
: wxApp()
{
return ;
}
bool MyApp::OnInit(void) {
bool retv = false ;
if (wxApp::OnInit()) {
wxWindow* p_mainwnd = new MainWindow() ;
this->SetTopWindow(p_mainwnd) ;
p_mainwnd->Show(true) ;
retv = true ;
}
return retv ;
}
MainWindow::MainWindow(void)
: wxFrame(NULL, wxID_ANY, "Test Scrolled+MinSize")
{
wxBoxSizer* const p_lm_root = new wxBoxSizer(wxHORIZONTAL) ;
this->SetSizer(p_lm_root) ;
// sidebar
#ifndef USE_SCROLLED
wxPanel* p_scroller = new wxPanel(this, wxID_ANY) ;
#else
wxScrolled<wxPanel>* const p_scroller
= new wxScrolled<wxPanel>(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, (wxVSCROLL)) ;
#endif
#ifdef USE_SCROLLRATE
p_scroller->SetScrollRate(1, 1) ;
#endif
p_lm_root->Add(p_scroller, 1, (wxEXPAND)) ;
wxBoxSizer* const p_lm_scroller = new wxBoxSizer(wxVERTICAL) ;
p_scroller->SetSizer(p_lm_scroller) ;
wxWindow* const p_sidebar = this->buildSideBar(p_scroller) ;
#ifndef USE_MINSIZE_3A
p_lm_scroller->Add(p_sidebar, 1) ;
#else
p_lm_scroller->Add(p_sidebar, 1, (wxEXPAND)) ;
#endif
#ifdef USE_MINSIZE_2
{
const int sidebar_w = p_sidebar->GetBestSize().GetWidth() ;
const wxSize size(sidebar_w, wxDefaultCoord) ;
std::cerr << "D: Sidebar scroller minsize.width set to: " << sidebar_w << std::endl ;
p_scroller->SetMinClientSize(size) ;
_minsize = sidebar_w ;
}
#endif
#ifdef USE_MINSIZE_3
{
const int sidebar_w = (p_sidebar->GetBestSize().GetWidth() + 20); // reserve 20 pixels more for vscrollbar
const wxSize size(sidebar_w, wxDefaultCoord) ;
std::cerr << "D: Sidebar scroller minsize.width set to: " << sidebar_w << std::endl ;
p_scroller->SetMinClientSize(size) ;
}
#endif
#ifdef USE_EVTHANDLER
_p_scroller = p_scroller ;
_p_sidebar = p_sidebar ;
this->Bind(wxEVT_SIZE, & MainWindow::handleSizeEvent, this) ;
#endif
// mainarea
wxScrolled<wxWindow>* const p_mainpanel = new wxScrolled<wxWindow>(this, wxID_ANY) ;
#ifdef USE_SCROLLRATE
p_mainpanel->SetScrollRate(1, 1) ; // Scrolled is like Panel or Window if this is comment out!!!
#endif
#ifdef USE_MINSIZE
p_mainpanel->SetMinSize(wxSize(100, 100)) ;
#endif
p_lm_root->Add(p_mainpanel, 8, (wxEXPAND)) ;
wxBoxSizer* const p_lm_mainpanel = new wxBoxSizer(wxVERTICAL) ;
p_mainpanel->SetSizer(p_lm_mainpanel) ;
wxWindow* const p_mainview = new wxWindow(p_mainpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, (wxBORDER_RAISED)) ;
p_mainview->SetMinClientSize(wxSize(300, 200)) ;
p_lm_mainpanel->Add(p_mainview, 0) ;
return ;
}
wxWindow* MainWindow::buildSideBar(wxWindow* const p_parentwnd) {
wxPanel* p_root = new wxPanel(p_parentwnd, wxID_ANY) ;
wxBoxSizer* p_lm_root = new wxBoxSizer(wxVERTICAL) ;
p_root->SetSizer(p_lm_root) ;
{
wxStaticText* p_text1 = new wxStaticText(p_root, wxID_ANY, wxT("Sample static text")) ;
wxTextCtrl* p_tc1 = new wxTextCtrl(p_root, wxID_ANY, wxT("Input text here")) ;
p_lm_root->Add(p_text1, 0, (wxEXPAND | wxALL), 4) ;
p_lm_root->Add(p_tc1, 0, (wxEXPAND | wxALL), 4) ;
}
{
wxStaticText* p_text1 = new wxStaticText(p_root, wxID_ANY, wxT("Sample static text")) ;
wxTextCtrl* p_tc1 = new wxTextCtrl(p_root, wxID_ANY, wxT("Input text here")) ;
p_lm_root->Add(p_text1, 0, (wxEXPAND | wxALL), 4) ;
p_lm_root->Add(p_tc1, 0, (wxEXPAND | wxALL), 4) ;
}
{
wxStaticText* p_text1 = new wxStaticText(p_root, wxID_ANY, wxT("Sample static text")) ;
wxTextCtrl* p_tc1 = new wxTextCtrl(p_root, wxID_ANY, wxT("Input text here")) ;
p_lm_root->Add(p_text1, 0, (wxEXPAND | wxALL), 4) ;
p_lm_root->Add(p_tc1, 0, (wxEXPAND | wxALL), 4) ;
}
{
wxStaticText* p_text1 = new wxStaticText(p_root, wxID_ANY, wxT("Sample static text")) ;
wxTextCtrl* p_tc1 = new wxTextCtrl(p_root, wxID_ANY, wxT("Input text here")) ;
p_lm_root->Add(p_text1, 0, (wxEXPAND | wxALL), 4) ;
p_lm_root->Add(p_tc1, 0, (wxEXPAND | wxALL), 4) ;
}
#ifdef USE_MINSIZE
p_root->SetMinSize(wxSize(200, wxDefaultCoord)) ;
#endif
return p_root ;
}
#ifdef USE_EVTHANDLER
void MainWindow::handleSizeEvent(wxEvent& evt) {
const int current_size = _p_scroller->GetClientSize().GetWidth() ;
std::cerr << "D: required clientarea minsize: " << _minsize << ", current clientarea size: " << current_size << std::endl ;
if (current_size != _minsize) {
std::cerr << "D: Adjusting layout" << std::endl ;
const wxSize size(_minsize, wxDefaultCoord) ;
std::cerr << "D: Setting sidebar scroller's client area min-width to: " << _minsize << std::endl ;
_p_scroller->SetMinClientSize(size) ;
}
evt.Skip() ;
return ;
}
#endif