Hi i have a borderless wxFrame but it is not resizable. I want to make the frame resizable just like when it is resizable with a border. I am using wxRESIZE_BORDER and wxBORDER_NONE style properties. I know i have to catch the mouse event manually and then implement it but am not able do that. I have also looked at the shaped sample directory but there also there is no "border-less resizable frame". I am using Ubuntu 18.04 and wxWidgets 3.1.5. Is this doable/possible in wxWidgets and is there any example of the same?
Here's an example of the basic machinery needed to allow a frame to be resized without using a resize border. It basically shows Igor's comments above.
#include "wx/wx.h"
#include <wx/timer.h>
#include <wx/dcbuffer.h>
#include <wx/version.h>
#if __WXGTK__
#define BORDERLESS_FRAME_STYLE (wxCAPTION | wxCLOSE_BOX)
#else
#define BORDERLESS_FRAME_STYLE (wxCAPTION | wxCLOSE_BOX | wxBORDER_NONE)
#endif // __WXGTK__
class MyFrame: public wxFrame
{
public:
MyFrame();
private:
enum ResizeMode
{
ResizeNone,
ResizeFromTop,
ResizeFromUpperLeft,
ResizeFromLeft,
ResizeFromLowerLeft,
ResizeFromBottom,
ResizeFromLowerRight,
ResizeFromRight,
ResizeFromUpperRight
};
// General event handlers
void OnBgPanelPaint(wxPaintEvent&);
// Event handlers for resizing
void OnLeftDownForResizeFromLowerRight(wxMouseEvent&);
void OnLeftDownForResizeFromLowerLeft(wxMouseEvent&);
void OnLeftUp(wxMouseEvent&);
void OnMouseCaptureLost(wxMouseCaptureLostEvent&);
void OnResizeTimer(wxTimerEvent&);
// Resizing helper functions
void DoDragBasedResize();
void StartResize(wxWindow*, const wxPoint&);
void CompleteResize(bool doFinalResize = false);
// Data and objects needed for resizing.
wxTimer m_resizeTimer;
int m_resizeFrequency;
int m_resizeAreaLength;
ResizeMode m_resizeMode;
wxPoint m_resizeStartPoint;
wxSize m_initialFrameSize;
wxPoint m_initialFramePosition;
wxWindow* m_clickToResizeFromLowerRightWindow;
wxWindow* m_clickToResizeFromLowerLeftWindow;
wxWindow* m_curResizeWindow;
// GUI controls
wxPanel* m_bgPanel;
};
MyFrame::MyFrame():wxFrame(NULL, wxID_ANY, "Resizing Demo", wxDefaultPosition,
wxSize(400, 300), BORDERLESS_FRAME_STYLE)
{
// Set up the UI.
m_bgPanel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxTAB_TRAVERSAL|wxFULL_REPAINT_ON_RESIZE);
m_bgPanel->SetBackgroundStyle(wxBG_STYLE_PAINT );
m_bgPanel->Bind(wxEVT_PAINT, &MyFrame::OnBgPanelPaint, this);
wxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
mainSizer->Add(m_bgPanel, wxSizerFlags(1).Expand());
SetSizer(mainSizer);
Layout();
// Initialize the data needed for resizing.
m_resizeMode = ResizeNone;
#if wxCHECK_VERSION(3,1,0)
m_resizeAreaLength = FromDIP(20);
#else
m_resizeAreaLength = 20;
#endif // wxCHECK_VERSION
m_resizeTimer.Bind(wxEVT_TIMER, &MyFrame::OnResizeTimer, this);
m_resizeFrequency = 50;
m_curResizeWindow = NULL;
// Set window and event handlers for resizing from lower right.
m_clickToResizeFromLowerRightWindow = m_bgPanel;
m_clickToResizeFromLowerRightWindow->Bind(wxEVT_LEFT_DOWN,
&MyFrame::OnLeftDownForResizeFromLowerRight, this);
m_clickToResizeFromLowerRightWindow->Bind(wxEVT_LEFT_UP,
&MyFrame::OnLeftUp, this);
m_clickToResizeFromLowerRightWindow->Bind(wxEVT_MOUSE_CAPTURE_LOST,
&MyFrame::OnMouseCaptureLost, this);
// Set window and event handlers for resizing from lower left.
m_clickToResizeFromLowerLeftWindow = m_bgPanel;
m_clickToResizeFromLowerLeftWindow->Bind(wxEVT_LEFT_DOWN,
&MyFrame::OnLeftDownForResizeFromLowerLeft, this);
m_clickToResizeFromLowerLeftWindow->Bind(wxEVT_LEFT_UP,
&MyFrame::OnLeftUp, this);
m_clickToResizeFromLowerLeftWindow->Bind(wxEVT_MOUSE_CAPTURE_LOST,
&MyFrame::OnMouseCaptureLost, this);
}
void MyFrame::OnLeftDownForResizeFromLowerLeft(wxMouseEvent& event)
{
wxPoint p = event.GetPosition();
wxSize sz = m_clickToResizeFromLowerLeftWindow->GetClientSize();
// Check if the click is in the lower left of the window.
if ( p.x < m_resizeAreaLength &&
sz.GetHeight() - p.y < m_resizeAreaLength )
{
StartResize(m_clickToResizeFromLowerLeftWindow, p);
m_resizeMode = ResizeFromLowerLeft;
SetTitle("Resize From lower left in progress...");
SetCursor(wxCursor(wxCURSOR_SIZENESW));
}
else
{
event.Skip();
}
}
void MyFrame::OnLeftDownForResizeFromLowerRight(wxMouseEvent& event)
{
wxPoint p = event.GetPosition();
wxSize sz = m_clickToResizeFromLowerRightWindow->GetClientSize();
// Check if the click is in the lower right of the window.
if ( sz.GetWidth() - p.x < m_resizeAreaLength &&
sz.GetHeight() - p.y < m_resizeAreaLength )
{
StartResize(m_clickToResizeFromLowerRightWindow, p);
m_resizeMode = ResizeFromLowerRight;
SetTitle("Resize from lower right in progress...");
SetCursor(wxCursor(wxCURSOR_SIZENWSE));
}
else
{
event.Skip();
}
}
void MyFrame::OnLeftUp(wxMouseEvent& event)
{
if ( m_resizeMode != ResizeNone )
{
CompleteResize(true);
}
else
{
event.Skip();
}
}
void MyFrame::OnMouseCaptureLost(wxMouseCaptureLostEvent&)
{
if ( m_resizeMode != ResizeNone )
{
CompleteResize(false);
}
}
void MyFrame::OnResizeTimer(wxTimerEvent&)
{
DoDragBasedResize();
}
void MyFrame::DoDragBasedResize()
{
wxMouseState ms = ::wxGetMouseState();
wxPoint curMousePsn = ms.GetPosition();
wxPoint dragVector = curMousePsn - m_resizeStartPoint;
wxSize newSize(m_initialFrameSize);
wxPoint newPsn(m_initialFramePosition);
if ( m_resizeMode == ResizeFromLowerRight )
{
newSize = wxSize(m_initialFrameSize.GetWidth() + dragVector.x,
m_initialFrameSize.GetHeight() + dragVector.y);
}
else if ( m_resizeMode == ResizeFromLowerLeft )
{
newSize = wxSize(m_initialFrameSize.GetWidth() - dragVector.x,
m_initialFrameSize.GetHeight() + dragVector.y);
newPsn = wxPoint(m_initialFramePosition.x + dragVector.x,
m_initialFramePosition.y);
}
SetSize(newPsn.x, newPsn.y, newSize.GetWidth(), newSize.GetHeight());
}
void MyFrame::StartResize(wxWindow* win, const wxPoint& p)
{
m_curResizeWindow = win;
m_resizeTimer.Start(m_resizeFrequency);
m_resizeStartPoint = m_curResizeWindow->ClientToScreen(p);
m_curResizeWindow->CaptureMouse();
m_initialFramePosition = GetPosition();
m_initialFrameSize = GetSize();
}
void MyFrame::CompleteResize(bool doFinalResize)
{
if ( doFinalResize )
{
DoDragBasedResize();
}
m_resizeTimer.Stop();
m_resizeMode = ResizeNone;
SetTitle("Resize complete");
SetCursor(wxCursor(wxCURSOR_ARROW));
if ( m_curResizeWindow && m_curResizeWindow->HasCapture() )
{
m_curResizeWindow->ReleaseMouse();
}
m_curResizeWindow = NULL;
}
void MyFrame::OnBgPanelPaint(wxPaintEvent&)
{
wxAutoBufferedPaintDC dc(m_bgPanel);
dc.Clear();
wxPen pen(*wxRED,5);
dc.SetPen(pen);
// Draw some red marks to indicate the lower right resize area
wxPoint lowerRight = m_bgPanel->GetClientRect().GetBottomRight();
dc.DrawLine(lowerRight - wxPoint(0,m_resizeAreaLength), lowerRight);
dc.DrawLine(lowerRight, lowerRight - wxPoint(m_resizeAreaLength,0));
// Draw some red marks to indicate the lower left resize area
wxPoint lowerLeft = m_bgPanel->GetClientRect().GetBottomLeft();
dc.DrawLine(lowerLeft - wxPoint(0,m_resizeAreaLength), lowerLeft);
dc.DrawLine(lowerLeft, lowerLeft + wxPoint(m_resizeAreaLength,0));
}
class MyApp : public wxApp
{
public:
virtual bool OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(MyApp);
This only shows resizing if the click is in the lower right corner of a panel covering the client area of the frame. I tried to add enough generality that if you have another control covering the lower right, you should be able to set that control to be m_clickToResizeWindow in the code above.
There is one case where that might not work however. I could be wrong, but I think some native controls completely consume mouse clicks and won't even generate a wxMouseEvent. In this case, resizing won't be possible if such a control is in the lower right.
There are some parameters that can be changed to modify the resizing behavior. The m_resizeAreaLength determines how close to the edge a click can be to start the resizing process. I've set this to 20 DIPs. The m_resizeFrequency determines how frequently the size is updated during a resize operation. I've set this to 50ms. A smaller value will provide smoother updates. In this example, I drew some red marks on the lower right to indicate the resizing area. This is completely unnecessary and can be removed.
This example only shows resizing based on a click on the lower right. However it shouldn't be too hard to modify the code to allow only horizontal resizing based on a click on the right edge or vertical resizing based on a click on the bottom edge. However, it might get complicated if there are multiple controls covering the left or bottom edges. This system only works well if there is one control covering those edges.
edit: I've updated the code to show resizing from both the lower left and lower right while keeping the rest of the frame in the appropriate position.
This can be further expanded to allow for resizing from other edges or corners. To do so it will be necessary to:
add a pointer for the window that will be used to receive the click starting the resize.
add a left down handler for that window to start the process.
Bind the new left down handler, the existing left up handler, and the existing capture lost handler for the window.
add to the DoDragBasedResize method setting a new size and position for the frame appropriate for the type of resizing being done.
Related
I am creating an animation timeline widget using wxWidgets.
The widget consist of a wxFrame with a wxScrolledWindow child:
I want to support horizontal zooming by changing the timeline window virtual size when the user rolls mouse middle button.
Zooming without scrolling works well. But when i start scrolling(forward and backward) things get messed up.
So to troubleshoot I decided to do a simple test:
Draw a line from the window top left corner to the bottom right corner. And the test fails!
Without scrolling -> Test pass
Zoom then scroll to right -> Test fails
Scroll back to left -> Test also fails
I have no idea of whats going on. Here my CPP code:
In .H:
class AnimationFrame : public wxFrame
{
public:
AnimationFrame();
private:
void onTimelineMouseWheel(wxMouseEvent& ev);
void onTimelinePaint(wxPaintEvent& ev);
// The scrolled window.
wxScrolledWindow* m_timelineWindow;
};
In .CPP:
AnimationFrame::AnimationFrame()
{
assert(wxXmlResource::Get()->LoadFrame(this, NULL, "AnimationFrame"));
m_timelineWindow = (wxScrolledWindow*)wxWindow::FindWindowByName("TimelineScrolledWindow", this);
assert(m_timelineWindow);
wxPanel* headerPanel = (wxPanel*)wxWindow::FindWindowByName("HeaderPanel", this);
assert(headerPanel);
// Adjust panel sizes.
{
headerPanel->SetMaxSize(wxSize(headerPanel->GetMaxSize().x, 40));
m_timelineWindow->SetMaxSize(wxSize(m_timelineWindow->GetMaxSize().x, 100));
this->SetMinSize(wxSize(this->GetSize().x, 250));
this->SetSize(wxSize(this->GetSize().x, 250));
this->SetMaxSize(wxSize(this->GetSize().x, 250));
}
// Timeline events from the scrolled window.
m_timelineWindow->Bind(wxEVT_MOUSEWHEEL, &AnimationFrame::onTimelineMouseWheel, this);
m_timelineWindow->Bind(wxEVT_PAINT, std::bind(&AnimationFrame::onTimelinePaint, this, std::placeholders::_1));
}
void AnimationFrame::onTimelineMouseWheel(wxMouseEvent& ev)
{
static const nbFloat32 scrollIntensity = 1.20f;
const nbFloat32 delta = scrollIntensity * (nbFloat32)(ev.GetWheelRotation() / ev.GetWheelDelta());
const nbFloat32 sizeFactor = delta >= 0.0 ? delta : 1.0f / -delta;
const wxSize currentVirtualSize = m_timelineWindow->GetVirtualSize();
wxSize targetVirtualSize = wxSize((nbUint32)((nbFloat32)currentVirtualSize.x * sizeFactor), currentVirtualSize.y);
targetVirtualSize.x = std::min(targetVirtualSize.x, 5000);
targetVirtualSize.x = std::max(targetVirtualSize.x, m_timelineWindow->GetSize().x);
m_timelineWindow->SetVirtualSize(targetVirtualSize);
this->Refresh();
}
void AnimationFrame::onTimelinePaint(wxPaintEvent& ev)
{
// Init the DC.
wxPaintDC dc(m_timelineWindow);
PrepareDC(dc);// from the scrolling sample. Doesnt seems to do anything.
/*
wxFont font(12, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
dc.SetFont(font);
dc.SetBackgroundMode(wxTRANSPARENT);
dc.SetTextForeground(*wxBLACK);
dc.SetTextBackground(*wxWHITE);
*/
// Now draw line.
dc.DrawLine(wxPoint(0, 0), wxPoint(m_timelineWindow->GetSize().x, m_timelineWindow->GetSize().y));
}
Addional information:
As you certainly realised the widget is loaded from CPP using the XRC format. Here the generated code from wxFormBuilder for the widget.
XRC: https://1drv.ms/u/s!Ak_u4fVrMHMkrTe59Fuo4X2b-fRO?e=2Z0lw0
CPP: https://1drv.ms/u/s!Ak_u4fVrMHMkrTi_8w5UIkwFTl9t?e=BZBlLs
Hi i am trying to create a wxApp that will have not have a titlebar(including the minimize, maximize and close) icons that are by default provided. The code that i have is as follows:
main.h
class MyApp : public wxApp
{
public:
virtual bool OnInit();
};
main.cpp
bool MyApp::OnInit()
{
MyFrame *prop = new MyFrame(wxT("MyFrame"));
prop->Show(true);
return true;
}
myframe.h
class MyFrame : public wxFrame
{
public:
MyFrame(const wxString& title);
void OnClick(wxMouseEvent& event);
};
myframe.cpp
MyFrame::MyFrame(const wxString& title)
: wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(250, 130))
{
MyPanel *panel = new MyPanel(this, wxID_ANY);
panel->SetBackgroundColour(wxColour(255,255,255));
MyButton *button = new MyButton(panel, wxID_ANY, wxT("Ok"));
Connect( wxEVT_LEFT_UP,
wxMouseEventHandler(MyFrame::OnClick));
panel->Centre();
}
void MyFrame::OnClick(wxMouseEvent& event)
{
std::cout << "event reached frame class" << std::endl;
}
mypanel.h
class MyPanel : public wxPanel
{
public:
MyPanel(wxFrame *frame, int id);
void OnClick(wxMouseEvent& event);
};
mypanel.cpp
MyPanel::MyPanel(wxFrame *frame, int id)
: wxPanel(frame, id)
{
Connect( wxEVT_LEFT_UP,
wxMouseEventHandler(MyPanel::OnClick));
}
void MyPanel::OnClick(wxMouseEvent& event)
{
std::cout << "event reached panel class" << std::endl;
}
mybutton.h
class MyButton : public wxPanel
{
public:
MyButton(wxPanel *panel, int id, const wxString &label);
void OnClick(wxMouseEvent& event);
};
mybutton.cpp
void MyButton::onClick(wxMouseEvent &event)
{
}
What i want is:
There should be no title bar(including the 3 maximize, minimize & close buttons) at the top.
Now since there is no titlebar at the top of the frame, there is no way to drag or close or maximize or minimize the window. For that i want to create a custom titlebar at the top which should have the three customized maximized,minimized and close button and also i should be able to drag the frame by double clicking and holding and dragging the topmost part of the newly created frame.
Is this possible in wxWidgets? If yes, how can i achieve this?
I am not proposing any new way of dragging. The new frame/window that we will have should also be dragged only by its own customized title bar. It's exactly like dragging the old frame by double clicking and dragging the frame in the native case. I just want to customize the native title bar. Like increase its height, change it's colour, change how to three buttons(minimize, maximize and close) look.
Here's the simplest example I can think of how to create a frame with a pseudo titlebar that can be clicked to drag the frame around. This example shows which mouse events need to be handled to drag the window around and how to do the calculations needed in those event handlers.
Note that moving the frame needs to be done in screen coordinates, but the coordinates received in the event handlers will be in client coordinates for the title bar. This example also shows how to do those coordinate conversions.
#include "wx/wx.h"
class CustomTitleBar:public wxWindow
{
public:
CustomTitleBar(wxWindow* p) : wxWindow(p,wxID_ANY)
{
m_dragging = false;
SetBackgroundColour(*wxGREEN);
Bind(wxEVT_LEFT_DOWN,&CustomTitleBar::OnMouseLeftDown,this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &CustomTitleBar::OnMouseCaptureLost,
this);
}
wxSize DoGetBestClientSize() const override
{
return wxSize(-1,20);
}
private:
void OnMouseLeftDown(wxMouseEvent& event)
{
if ( !m_dragging )
{
Bind(wxEVT_LEFT_UP,&CustomTitleBar::OnMouseLeftUp,this);
Bind(wxEVT_MOTION,&CustomTitleBar::OnMouseMotion,this);
m_dragging = true;
wxPoint clientStart = event.GetPosition();
m_dragStartMouse = ClientToScreen(clientStart);
m_dragStartWindow = GetParent()->GetPosition();
CaptureMouse();
}
}
void OnMouseLeftUp(wxMouseEvent&)
{
FinishDrag();
}
void OnMouseMotion(wxMouseEvent& event)
{
wxPoint curClientPsn = event.GetPosition();
wxPoint curScreenPsn = ClientToScreen(curClientPsn);
wxPoint movementVector = curScreenPsn - m_dragStartMouse;
GetParent()->SetPosition(m_dragStartWindow + movementVector);
}
void OnMouseCaptureLost(wxMouseCaptureLostEvent&)
{
FinishDrag();
}
void FinishDrag()
{
if ( m_dragging )
{
Unbind(wxEVT_LEFT_UP,&CustomTitleBar::OnMouseLeftUp,this);
Unbind(wxEVT_MOTION,&CustomTitleBar::OnMouseMotion,this);
m_dragging = false;
}
if ( HasCapture() )
{
ReleaseMouse();
}
}
wxPoint m_dragStartMouse;
wxPoint m_dragStartWindow;
bool m_dragging;
};
class Customframe : public wxFrame
{
public:
Customframe(wxWindow* p)
:wxFrame(p, wxID_ANY, wxString(), wxDefaultPosition, wxSize(150,100),
wxBORDER_NONE)
{
CustomTitleBar* t = new CustomTitleBar(this);
SetBackgroundColour(*wxBLUE);
wxBoxSizer* szr = new wxBoxSizer(wxVERTICAL);
szr->Add(t,wxSizerFlags(0).Expand());
SetSizer(szr);
Layout();
}
};
class MyFrame: public wxFrame
{
public:
MyFrame():wxFrame(NULL, wxID_ANY, "Custom frame Demo", wxDefaultPosition,
wxSize(400, 300))
{
wxPanel* bg = new wxPanel(this, wxID_ANY);
wxButton* btn = new wxButton(bg, wxID_ANY, "Custom frame");
wxBoxSizer* szr = new wxBoxSizer(wxVERTICAL);
szr->Add(btn,wxSizerFlags(0).Border(wxALL));
bg->SetSizer(szr);
Layout();
btn->Bind(wxEVT_BUTTON, &MyFrame::OnButton, this);
m_customFrame = NULL;
}
private:
void OnButton(wxCommandEvent&)
{
if ( m_customFrame )
{
m_customFrame->Close();
m_customFrame = NULL;
}
else
{
m_customFrame = new Customframe(this);
m_customFrame->CenterOnParent();
m_customFrame->Show();
}
}
wxFrame* m_customFrame;
};
class MyApp : public wxApp
{
public:
virtual bool OnInit()
{
MyFrame* frame = new MyFrame();
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(MyApp);
On windows, it looks like this.
You should be able to add whatever buttons you want to the custom title bar exactly as you would add buttons to any other window.
"Is this possible in wxWidgets?"
Yes. You need to use a wxWindow instead of a wxFrame and set some style for it. like wxBORDER_NONE. But you will have to implement many things that wxFrame already provides.
And your proposed way of dragging seems wrong/confusing to me. 99.99% of users prefer a UI they are used to it, avoiding to learn a new way of doing simple things they already know.
If you just want to avoid resizing then you have two ways:
a) Catch the size-event and do nothing. Calling event.Skip(false)
(to prevent handling in parent) is not neccessary.
b) Once you have created your window and correctly sized, get its size and set it as max and min.
In both cases the user will see a "resize" mouse pointer when hovering the mouse at any border, but nothing else, no resizing, will be done.
#JasonLiam,
Don't forget to place the application icon in the top left corner of you title bar and handle the right/left mouse clicks appropriately (as in native application) (if you want to go this route and get rid of the native frame window).
Thank you.
I have created a widget class (in C++) ToolTray which is basically few QToolButtons added in QHboxLayout. These buttons are used by the user for operations like Save, Open etc. Initially, the buttons are added with toolButtonStyle set to Qt::ToolButtonTextBesideIcon.
I want to change the toolButtonStyle to Qt::ToolButtonIconOnly in resizeEvent if the new size is not sufficient to display text and icons of QToolButton. See the picture below:
If the window is resized and if the new size is sufficient to display the text and icon of all the QToolButton, then toolButtonStyle should be changed back to Qt::ToolButtonTextBesideIcon.
I tried to achieve this with following code:
void ToolTray::resizeEvent(QResizeEvent *event)
{
int totalWidth = 0;
bool mode = runBtn_->toolButtonStyle() == Qt::ToolButtonStyle::ToolButtonIconOnly;
// Mode => True => For ICON Only
// Mode => False => For ICON + Text
for (auto btn: toolBtns_) {
if (btn->isVisible())
totalWidth += btn->size().width();
}
qDebug() << "Total Width: " << totalWidth ;
qDebug() << "Event Size: " << event->size() << " Old size " << event->oldSize();
if (event->oldSize().isEmpty()) // Ignore ResizeEvent for QSize(-1,-1)
return;
if (mode) { // Already Small
if (event->size().width() < preferedFullWidth_)
return;
for (auto btn: toolBtns_) {
if (btn == moreBtn_)
continue;
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
}
return;
}
// The QToolButtons are Text Beside Icon
if (event->size().width() >= totalWidth)
return;
qDebug() << "Here";
for (auto btn: toolBtns_)
btn->setToolButtonStyle(Qt::ToolButtonIconOnly);
preferedFullWidth_ = totalWidth;
}
However, I am unable to achieve what I wanted:
Whenever I try to shrink the size of the window, the QToolButton's Text first start clipping and after some more size reduction, the ToolButtonStyle is changed to Qt::ToolButtonIconOnly. I don't want clipping to happen.
I also want to add some margin (or hysteresis) in my implementation. Like once the QToolButton is changed to Qt::ToolButtonIconOnly at a particular width, then the width should be greater than preferedFullWidth_ + certain margin to switch back to Qt::ToolButtonTextBesideIcon.
Toolbars seem to be a theme this week... :)
So you basically have the right idea but the tricky part is figuring out the required size. Unfortunately the QToolBar layout is completely private so we have to figure stuff out on our own (even though it inherits from QLayout you can't get an instance of it via QToolBar::layout()).
This implementation is fairly basic and will probably not handle all cases (I only tested with basic actions, no custom widgets or such), but it does work for that (tested on Windows and Linux with various styles).
ADDED: I realize you weren't asking for a QToolBar specifically, but that's what you essentially described... I'm not sure why re-invent that wheel (a QToolBar can be placed in any layout, doesn't have to be in a main window), but if you were to implement your own version I think a lot of this example would still apply. I would also personally almost always use QActions for triggering UI events (vs. buttons) since they can be assigned to any number of UI elements (toolbar, manu bar, context menu, window shortcuts, etc.).
Adding a margin/hysteresis is left as an exercise for the reader... :) I don't think it needs one, but you could pad m_expandedSize in initSizes() with some arbitrary margin.
CollapsingToolBar
#include <QtWidgets>
class CollapsingToolBar : public QToolBar
{
Q_OBJECT
public:
explicit CollapsingToolBar(QWidget *parent = nullptr) : CollapsingToolBar(QString(), parent) {}
explicit CollapsingToolBar(const QString &title, QWidget *parent = nullptr) :
QToolBar(title, parent)
{
initSizes();
// If icon sizes change we need to recalculate all the size hints, but we need to wait until the buttons have adjusted themselves, so we queue the update.
connect(this, &QToolBar::iconSizeChanged, [this](const QSize &) {
QMetaObject::invokeMethod(this, "recalcExpandedSize", Qt::QueuedConnection);
});
// The drag handle can mess up our sizing, update preferred size if it changes.
connect(this, &QToolBar::movableChanged, [this](bool movable) {
const int handleSz = style()->pixelMetric(QStyle::PM_ToolBarHandleExtent, nullptr, this);;
m_expandedSize = (movable ? m_expandedSize + handleSz : m_expandedSize - handleSz);
adjustForSize();
});
}
protected:
// Monitor action events to keep track of required size.
void actionEvent(QActionEvent *e) override
{
QToolBar::actionEvent(e);
int width = 0;
switch (e->type())
{
case QEvent::ActionAdded:
// Personal pet-peeve... optionally set buttons with menus to have instant popups instead of splits with the main button doing nothing.
//if (QToolButton *tb = qobject_cast<QToolButton *>(widgetForAction(e->action())))
// tb->setPopupMode(QToolButton::InstantPopup);
//Q_FALLTHROUGH;
case QEvent::ActionChanged:
width = widthForAction(e->action());
if (width <= 0)
return;
if (e->type() == QEvent::ActionAdded || !m_actionWidths.contains(e->action()))
m_expandedSize += width + m_spacing;
else
m_expandedSize = m_expandedSize - m_actionWidths.value(e->action()) + width;
m_actionWidths.insert(e->action(), width);
break;
case QEvent::ActionRemoved:
if (!m_actionWidths.contains(e->action()))
break;
width = m_actionWidths.value(e->action());
m_expandedSize -= width + m_spacing;
m_actionWidths.remove(e->action());
break;
default:
return;
}
adjustForSize();
}
bool event(QEvent *e) override
{
// Watch for style change
if (e->type() == QEvent::StyleChange)
recalcExpandedSize();
return QToolBar::event(e);
}
void resizeEvent(QResizeEvent *e) override
{
adjustForSize();
QToolBar::resizeEvent(e);
}
private slots:
// Here we do the actual switching of tool button style based on available width.
void adjustForSize()
{
int availableWidth = contentsRect().width();
if (!isVisible() || m_expandedSize <= 0 || availableWidth <= 0)
return;
switch (toolButtonStyle()) {
case Qt::ToolButtonIconOnly:
if (availableWidth > m_expandedSize)
setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
break;
case Qt::ToolButtonTextBesideIcon:
if (availableWidth <= m_expandedSize)
setToolButtonStyle(Qt::ToolButtonIconOnly);
break;
default:
break;
}
}
// Loops over all previously-added actions and re-calculates new size (eg. after icon size change)
void recalcExpandedSize()
{
if (m_actionWidths.isEmpty())
return;
initSizes();
int width = 0;
QHash<QAction *, int>::iterator it = m_actionWidths.begin();
for ( ; it != m_actionWidths.end(); ++it) {
width = widthForAction(it.key());
if (width <= 0)
continue;
m_expandedSize += width + m_spacing;
it.value() = width;
}
adjustForSize();
}
private:
void initSizes()
{
// Preload some sizes based on style settings.
// This is the spacing between items
m_spacing = style()->pixelMetric(QStyle::PM_ToolBarItemSpacing, nullptr, this);
// Size of a separator
m_separatorWidth = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, nullptr, this);
// The layout margins (we can't even get the private QToolBarLayout via layout() so we figure it out like it does)
m_expandedSize = (style()->pixelMetric(QStyle::PM_ToolBarItemMargin, nullptr, this) + style()->pixelMetric(QStyle::PM_ToolBarFrameWidth, nullptr, this)) * 2;
// And the size of the drag handle if we have one
if (isMovable())
m_expandedSize += style()->pixelMetric(QStyle::PM_ToolBarHandleExtent, nullptr, this);
}
int widthForAction(QAction *action) const
{
// Try to find how wide the action representation (widget/separator) is.
if (action->isSeparator())
return m_separatorWidth;
if (QToolButton *tb = qobject_cast<QToolButton *>(widgetForAction(action))) {
const Qt::ToolButtonStyle oldStyle = tb->toolButtonStyle();
// force the widest size
tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
const int width = tb->sizeHint().width();
tb->setToolButtonStyle(oldStyle);
return width;
}
if (const QWidget *w = widgetForAction(action))
return w->sizeHint().width();
return 0;
}
int m_expandedSize = -1; // The maximum size we need with all buttons expanded and allowing for margins/etc
int m_spacing = 0; // Layout spacing between items
int m_separatorWidth = 0; // Width of separators
QHash<QAction *, int> m_actionWidths; // Use this to track action additions/removals/changes
};
Test/demo
// An XPM icon ripped from QCommonStyle
static const char * const info_xpm[]={
"32 32 5 1",
". c None",
"c c #000000",
"* c #999999",
"a c #ffffff",
"b c #0000ff",
"...........********.............",
"........***aaaaaaaa***..........",
"......**aaaaaaaaaaaaaa**........",
".....*aaaaaaaaaaaaaaaaaa*.......",
"....*aaaaaaaabbbbaaaaaaaac......",
"...*aaaaaaaabbbbbbaaaaaaaac.....",
"..*aaaaaaaaabbbbbbaaaaaaaaac....",
".*aaaaaaaaaaabbbbaaaaaaaaaaac...",
".*aaaaaaaaaaaaaaaaaaaaaaaaaac*..",
"*aaaaaaaaaaaaaaaaaaaaaaaaaaaac*.",
"*aaaaaaaaaabbbbbbbaaaaaaaaaaac*.",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
".*aaaaaaaaaaabbbbbaaaaaaaaaac***",
".*aaaaaaaaaaabbbbbaaaaaaaaaac***",
"..*aaaaaaaaaabbbbbaaaaaaaaac***.",
"...caaaaaaabbbbbbbbbaaaaaac****.",
"....caaaaaaaaaaaaaaaaaaaac****..",
".....caaaaaaaaaaaaaaaaaac****...",
"......ccaaaaaaaaaaaaaacc****....",
".......*cccaaaaaaaaccc*****.....",
"........***cccaaaac*******......",
"..........****caaac*****........",
".............*caaac**...........",
"...............caac**...........",
"................cac**...........",
".................cc**...........",
"..................***...........",
"...................**..........."};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent)
{
QToolBar* tb = new CollapsingToolBar(this);
tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
tb->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
QIcon icon = QIcon(QPixmap(info_xpm));
for (int i=0; i < 6; ++i)
tb->addAction(icon, QStringLiteral("Action %1").arg(i));
addToolBar(tb);
show();
// Adding another action after show() may collapse all the actions if the new toolbar preferred width doesn't fit the window.
// Only an issue if the toolbar size hint was what determined the window width to begin with.
//tb->addAction(icon, QStringLiteral("Action After"));
// Test setting button style after showing (comment out the one above)
//tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
// Test changing icon size after showing.
//tb->setIconSize(QSize(48, 48));
// Try this too...
//tb->setMovable(false);
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
//QApplication::setStyle("Fusion");
//QApplication::setStyle("windows");
MainWindow w;
return app.exec();
}
I have written a program in c++ using wxwidgets.I am placing a rectangle on the image and want to select the part of image covered by rectangle for which the rectangle should be draggable. But the problem is when I click the mouse the image vanishes and the only rectangle (which can be dragged) remains and it happens vice-versa.
`
class BasicDrawPane : public wxPanel
{
public:
BasicDrawPane();
BasicDrawPane(wxFrame* parent);
void paintEvent(wxPaintEvent & evt);
void render(wxDC& dc);
void mouseMoved(wxMouseEvent& event);
void mouseDown(wxMouseEvent& event);
void mouseWheelMoved(wxMouseEvent& event);
void mouseReleased(wxMouseEvent& event);
void rightClick(wxMouseEvent& event);
void mouseLeftWindow(wxMouseEvent& event);
DECLARE_EVENT_TABLE()
};
class MyFrame: public wxFrame{
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
wxString path;
BasicDrawPane panel;
private:
void OnHello(wxCommandEvent& event);
void OnExit(wxCommandEvent& event);
void OnAbout(wxCommandEvent& event);
void OnOpen(wxCommandEvent& event);
void OnPaint(wxCommandEvent& event);
void OnRect(wxCommandEvent& event);
void OnSave(wxCommandEvent& event);
DECLARE_EVENT_TABLE();
wxBitmap bmp;
wxMemoryDC memDC;
};
enum
{
ID_Hello = 1, ID_PAINT = 2, ID_RECT = 3, ID_SAVE = 4
};
BEGIN_EVENT_TABLE( MyFrame, wxFrame )
EVT_MENU(ID_Hello,MyFrame::OnHello)
EVT_MENU(wxID_EXIT,MyFrame::OnExit)
EVT_MENU(wxID_ABOUT,MyFrame::OnAbout)
EVT_MENU(wxID_OPEN,MyFrame::OnOpen)
EVT_MENU(ID_PAINT,MyFrame::OnPaint)
EVT_MENU(ID_RECT,MyFrame::OnRect)
EVT_MENU(ID_SAVE,MyFrame::OnSave)
END_EVENT_TABLE()
void MyFrame::OnPaint(wxCommandEvent& event)
{
//wxPaintDC dc( this );
//dc.DrawBitmap( m_bitmap, 0, 0, true /* use mask */ );
//wxStaticBitmap *b1 = new wxStaticBitmap(this, -1, wxBitmap(wxImage(path)));
bmp.LoadFile((path),wxBITMAP_TYPE_ANY);
// bmp.LoadFile((path),wxBITMAP_TYPE_PNG);
memDC.SelectObject( bmp );
//memDC.SetBackground(*wxWHITE_BRUSH);
//memDC.Clear();
/* memDC.SetPen(*wxGREEN_PEN);
memDC.SetBrush(*wxTRANSPARENT_BRUSH);
memDC.DrawRectangle( m_x, m_y, WIDTH, HEIGHT );*/
//Check();
memDC.SelectObject(wxNullBitmap);
// wxSize sz(512,384);
// wxSize sz(900,600);
wxStaticBitmap *b1 = new wxStaticBitmap(/* dynamic_cast<wxFrame*>*/this, -1, bmp, wxDefaultPosition);
Refresh();
}
class MyApp: public wxApp
{
public:
virtual bool OnInit();
//MyFrame *frame;
BasicDrawPane * drawPane;
};
IMPLEMENT_APP(MyApp)
bool MyApp::OnInit()
{
// wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
//frame = new MyFrame((wxFrame *)NULL, -1, wxT("Hello wxDC"), wxPoint(50,50), wxSize(800,600));
MyFrame *frame = new MyFrame( _T("Hello World"), wxPoint(50, 50), wxSize(600, 600) );
// drawPane = new BasicDrawPane( (wxFrame*) frame );
// sizer->Add(drawPane, 1, wxEXPAND);
//frame->SetSizer(sizer);
// /* dynamic_cast<wxFrame*>(this)*/ frame-> SetAutoLayout(true);
/* dynamic_cast<wxFrame*>(this)*/frame -> Show();
return true;
}
BEGIN_EVENT_TABLE(BasicDrawPane, wxPanel)
EVT_MOTION(BasicDrawPane::mouseMoved)
EVT_LEFT_DOWN(BasicDrawPane::mouseDown)
EVT_LEFT_UP(BasicDrawPane::mouseReleased)
EVT_RIGHT_DOWN(BasicDrawPane::rightClick)
EVT_LEAVE_WINDOW(BasicDrawPane::mouseLeftWindow)
EVT_MOUSEWHEEL(BasicDrawPane::mouseWheelMoved)
EVT_PAINT(BasicDrawPane::paintEvent)
// catch paint events
END_EVENT_TABLE()
void BasicDrawPane::mouseDown(wxMouseEvent& event)
{
/* if (event.GetPosition().x >= m_x && event.GetPosition().x <= m_x + WIDTH &&
event.GetPosition().y >= m_y && event.GetPosition().y <= m_y + HEIGHT)
{
m_dragging = true;
m_previous_mouse_x = event.GetPosition().x;
m_previous_mouse_y = event.GetPosition().y;
}*/
}
void BasicDrawPane::mouseWheelMoved(wxMouseEvent& event) {}
void BasicDrawPane::mouseReleased(wxMouseEvent& event)
{
m_dragging = true;
}
void BasicDrawPane::mouseMoved(wxMouseEvent& event)
{
if (m_dragging && event.Dragging())
{
int delta_x = event.GetPosition().x - m_previous_mouse_x;
int delta_y = event.GetPosition().y - m_previous_mouse_y;
m_x += delta_x;
m_y += delta_y;
m_previous_mouse_x = event.GetPosition().x;
m_previous_mouse_y = event.GetPosition().y;
// trigger paint event
Refresh();
}
}
void BasicDrawPane::mouseLeftWindow(wxMouseEvent& event)
{
m_dragging = true;
}
void BasicDrawPane::rightClick(wxMouseEvent& event) {}
BasicDrawPane::BasicDrawPane(wxFrame* parent) :
wxPanel(parent)
{
// m_dragging = true;
// m_x = 100;
// m_y = 100;
}
/*
* Called by the system of by wxWidgets when the panel needs
* to be redrawn. You can also trigger this call by
* calling Refresh()/Update().
*/
void BasicDrawPane::paintEvent(wxPaintEvent & evt)
{
//wxCommandEvent w1(wxEVT_NULL, ID_PAINT);
//OnPaint(w1);
wxPaintDC dc(this);
render(dc);
}
void BasicDrawPane::render(wxDC& dc)
{
dc.SetPen(*wxGREEN_PEN);
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle( m_x, m_y, WIDTH, HEIGHT );
}
`
There are several things to explain in order to answer this question, so I will take them one at a time. I think your basic idea is okay, so I won't go into a lot of detail on how the actual selection should take place etc.
Firstly, I would recommend to use either Connect() or Bind() instead of an event table. This allows you to connect child window events back to the parent window and handle them all in one place.
For example, if your main frame class is called MainFrame and you have a wxPanel member called m_DrawPanel, in the MainFrame ctor you could have:
MainFrame::MainFrame(wxWindow* parent)
{
// Connect mouse event handlers.
m_DrawPanel->Connect(wxEVT_LEFT_DOWN,
wxMouseEventHandler(MainFrame::OnPanelLDown), NULL, this);
m_DrawPanel->Connect(wxEVT_LEFT_UP,
wxMouseEventHandler(MainFrame::OnPanelLUp), NULL, this);
m_DrawPanel->Connect(wxEVT_MOTION,
wxMouseEventHandler(MainFrame::OnPanelMotion), NULL, this);
// Connect paint and erase handlers.
m_DrawPanel->Connect(wxEVT_PAINT,
wxPaintEventHandler(MainFrame::OnPanelPaint), NULL, this);
m_DrawPanel->Connect(wxEVT_ERASE_BACKGROUND,
wxEraseEventHandler(MainFrame::OnPanelErase), NULL, this);
// Load the bitmap and set the mode to 'not currently selecting'.
m_Picture.LoadFile ("wxwidgets.png", wxBITMAP_TYPE_PNG);
m_SelectionMode = false;
}
Note: I included a wxEVT_ERASE_BACKGROUND event override because otherwise panels tend to be cleared which leads to flicker (this is one simple approach).
The 3 mouse event handlers can implement your selection logic (I think this is basically what you are intending already):
void MainFrame::OnPanelLDown(wxMouseEvent& event)
{
m_SelectionMode = true;
m_SelectionRect = wxRect(event.GetPosition(), wxSize(0, 0));
}
void MainFrame::OnPanelLUp(wxMouseEvent& event)
{
m_SelectionMode = false;
// ... handle what to do with the selection here
// (selected area is defined by m_SelectionRect).
// ...
// Zero the selection rectangle for neatness (not really required).
m_SelectionRect = wxRect ();
}
void MainFrame::OnPanelMotion(wxMouseEvent& event)
{
m_SelectionRect = wxRect(m_SelectionRect.GetTopLeft(), event.GetPosition());
// Call Refresh() to trigger a paint event.
m_mainPanel->Refresh();
}
As mentioned earlier, override the panel's wxEVT_ERASE_BACKGROUND event to do nothing:
void MainFrame::OnPanelErase(wxEraseEvent& event)
{
}
Finally, I think this is the bit that you really are asking in your question (I included the others to help you build a working program):
void MainFrame::OnPanelPaint(wxPaintEvent& event)
{
// Obtain a wxPaintDC.
wxPaintDC pdc (m_mainPanel);
// Draw our image.
pdc.DrawBitmap(m_Picture, wxPoint(0, 0));
// If the user is currently selecting (left mouse button is down)
// then draw the selection rectangle.
if (m_SelectionMode)
{
pdc.SetPen(*wxRED_PEN);
pdc.SetBrush(*wxTRANSPARENT_BRUSH);
pdc.DrawRectangle(m_SelectionRect);
}
}
This is a paint event handler, so first we need to create a wxPaintDC context. Next, we paint the bitmap, this ensures it is refreshed every time and not damaged by mouse movements, resizing or other windows being dragged etc. Finally, if the user is currently moving the mouse with the left button pressed, then draw the selection rectangle.
There are many other ways of achieving the same thing. Some of them are possibly better, or more efficient, however this is a simple working way until you become more familiar with wxWidgets.
Windows 7 has this functionality where if a window is dragged to the side of the screen, it is maximized to take half of it. My problem is - I am trying to implement restore for widget's size and position and when Windows 7 "maximizes" the widget, qt still returns its position and size as if it was still shown normally, i.e - completely incorrect position and size.
Is there any control over this in qt5? I can't find it anywhere in the docs and its strange
This is essentially what I ended up doing.
"framegeometry" part at the bottom is necessary to compensate for widget's borders it seems.
QT += gui-private
is needed in the .pro file for the code below to work
#include <WinUser.h>
#include <qpa/qplatformnativeinterface.h>
static QWindow* windowForWidget(const QWidget* widget)
{
QWindow* window = widget->windowHandle();
if (window)
return window;
const QWidget* nativeParent = widget->nativeParentWidget();
if (nativeParent)
return nativeParent->windowHandle();
return 0;
}
#include <QWindow>
static HWND getHWNDForWidget(const QWidget* widget)
{
QWindow* window = ::windowForWidget(widget);
if (window && window->handle())
{
QPlatformNativeInterface* natInterface = QGuiApplication::platformNativeInterface();
return static_cast<HWND>(natInterface->nativeResourceForWindow(QByteArrayLiteral("handle"), window));
}
return 0;
}
void SaveSize(QWidget* w)
{
QSize size;
QPoint pos;
RECT pRect = { 0 };
HWND hwnd = getHWNDForWidget(w);
GetWindowRect(hwnd, &pRect);
auto left = w->frameGeometry().left();
auto right = w->frameGeometry().right();
auto width = w->width();
pos.setX(pRect.left);
pos.setY(pRect.top);
size.setWidth(pRect.right - pRect.left - (right - left - width));
size.setHeight(pRect.bottom - pRect.top);
//.... the rest of the code
}