How to prevent mouseMoveEvent on QCursor::setPos() using Qt? - c++

I am currently developing on an image viewer application. In this application I have a so called "pan-zoom" feature. This means that, when holding a certain mouse button, the user can zoom the image by panning forth and back.
It works fine, but as the feature is used, the mouse (naturally) moves up and down on the screen and will at some point reach the screen borders, which will make it stop. Instead I would like a behaviour where the mouse remains stationary and only the image magnification changes.
I tried to achieve this by invoking QCursor::setPos inside the QWidget::mouseMoveEvent and reset the mouse to the initial position after I have processed the move. It works so far as that the mouse is staying nearly stationary (it's wiggling forth and back). However, this will cause the mouse move event to be called again effectively annulling the adjustment I just made. This will result in a "wiggling" effect. Every adjustment will immediately be reversed.
Here is a code snipped, so you get an idea of what I am doing:
void ImageView::mouseMoveEvent(QMouseEvent *e) {
//some code
if (_panZooming) {
//some code here
//doesn't work as expected because it invokes this event again
QCursor::setPos(mapToGlobal(_initialMousePosition.toPoint()));
}
}
Is there a way to prevent the mouse move event to happen when using QCursor::setPos?

Assuming you're not calling the base class mouseMoveEvent, you should accept the event to mark it as being handled. By default, they're accepted when you re-implement the event, but it's clearer to be explicit. Call e->accept( ).
It's also recommended that if you handle any of the mouse events, you should handle all, with the possible exception of mouse double click.
Here's an example of keeping the mouse still, though on OS X there's an occasional flicker which appears to be due to how Qt is handling the events
class MyWidget : public QWidget
{
void mousePressEvent(QMouseEvent* e)
{
m_pos = e->globalPos();
m_lastPos = m_pos;
QWidget::mousePressEvent(e);
}
void mouseMoveEvent(QMouseEvent* e)
{
// Calculate relative zoom factor
// scaled down ( / 10 ) for image zooming
m_zoomFactor += ((float)e->globalPos().y() - m_lastPos.y()) / 10;
QCursor::setPos(m_pos);
m_lastPos = m_pos;
e->accept();
qDebug() << m_zoomFactor << endl;
}
void mouseReleaseEvent(QMouseEvent* e)
{
QWidget::mouseReleaseEvent(e);
}
private:
QPoint m_pos;
QPoint m_lastPos;
float m_zoomFactor = 0; // C++ 11 initialisation
};
If you're not bothered at keeping the mouse stationary, take out the QCursor::setPos call and this will still receive move events when the cursor is outside the widget, whilst the mouse button is held down.
However, it may be a better user experience hiding the cursor when zooming.

I would have a flag to disable the event with will be false by default.
inside the event check if flag is false, then perform the zoom operation, set flag to true and reset cursor.
then the event will be called again and the flag will be true, so you set flag to false and you will be ready to handle the next event.
You just have to make sure you dont have two or more calls to the mouse event firing from the actual mouse before receiving the event from the setCursor call.

Don't use event->pos() in mouse events, use QCursor::pos() intead and check if it changed. Like this:
void MyWidget::mousePressEvent(QMouseEvent *)
{
mPrevPos=QCursor::pos();
mMoving=false;
}
void MyWidget::mouseMoveEvent(QMouseEvent *)
{
auto cursorPos=QCursor::pos();
if(mPressedPos==cursorPos){
return;
}
if(!mMoving
&& (cursorPos-mPrevPos).manhattanLength()>QApplication::startDragDistance()){
mMoving=true;
}
if(mMoving){
auto diff=cursorPos-mPrevPos;
// move something using diff
QCursor::setPos(mPrevPos);
}
}
void MyWidget::mouseReleaseEvent(QMouseEvent *)
{
mMoving=false;
}
void MyWidget::leaveEvent(QEvent *)
{
mMoving=false;
}

Related

Jittery movement of QScrollArea from custom touchscreen driver

So a few years ago, I wrote a custom touchscreen driver specifically for a particular application which ran on a Scientific Linux 6.4 (CentOS 6 based) OS, which did not have native touch support, but the kernel supported touch events, so I was able to directly read the raw data from the touchscreen in /dev/input/event* and read the event data to generate mouse events with Qt to control the application to mimic a multi-touch touchscreen. More recently, we've finally migrated to RedHat 8.4, but I had to disable the native touch driver, because as far as I know, the native Qt touch events didn't allow the same degree of control over the application that my driver did.
Recently during a trial, a technician reported that when using the touchscreen to manipulate one of the application's image display areas, the movement was very "jittery". The image display area is simply a QScrollArea with a QImage inside, and hidden scroll bars. The way it works is that on a mouseMoveEvent, it manipulates the scrollbars according to the delta value on the mouse event.
void PanArea::mouseMoveEvent(QMouseEvent* e)
{
SafeStr str;
if (m_pw)
{
if (m_disable_panning == false &&
e->buttons().testFlag(Qt::RightButton))
{
QPoint delta = e->pos() - m_last_pos;
horizontalScrollBar()->setValue(horizontalScrollBar()->value() -
delta.x());
verticalScrollBar()->setValue(verticalScrollBar()->value() -
delta.y());
m_last_pos = e->pos();
emit signalScrollPositionChanged(getScrollPosition());
// This force update has been added because
// when fast panning cause black to appear within the image
// because some pan widget updates were being skipped.
// Performance seems acceptable with this here.
m_pw->update();
}
else if (...)
{
// irrelevant code removed to save space
}
}
}
This works fine when simply right-click dragging on the scroll area. And then here is the relevant function that dispatches the mouse press and mouse move events from the touchscreen driver:
void InputHandler::panStart(TouchPoint* tp, QWidget* widget)
{
QPoint pos(tp->cx(), tp->cy());
QWidget* target = widget;
if (target == NULL) target = QApplication::widgetAt(pos);
if (target != NULL)
{
QPoint local = target->mapFromGlobal(pos);
QMouseEvent* press =
new QMouseEvent(QEvent::MouseButtonPress, local, pos,
Qt::RightButton, Qt::RightButton, Qt::NoModifier);
QApplication::postEvent(widget, press);
}
}
void InputHandler::panMove(TouchPoint* tp, QWidget* widget)
{
QPoint pos(tp->cx(), tp->cy());
QWidget* target = widget;
if (target == NULL) target = QApplication::widgetAt(pos);
if (target != NULL)
{
QPoint local = target->mapFromGlobal(pos);
QMouseEvent* move =
new QMouseEvent(QEvent::MouseMove, local, pos, Qt::NoButton,
Qt::RightButton, Qt::NoModifier);
QApplication::postEvent(widget, move);
}
}
When I touch and drag on the widget, it DOES move, but it jumps all over the place as if I were rapidly dragging it in random directions.
Is there some caveat of how Qt5 mouse events work that could explain this behavior? Or something to do with the way the widget is moving the image around?

Some controls are not drawing, seemingly at random

I'm trying to write a little MFC app just for myself, to test some AI's I'm training.
So I added a picture control and a static control where I can paint stuff freely in the OnPaint() method of my main Window.
It seems to work when just drawing my app once, but I now added a loop that performs OnPaint() multiple times before stopping.
When in this loop, some other controls don't show up, for example all my buttons are gone, and some sliders even are missing some times, but other times, they're there.
My code goes like this:
void CKiUebung1Dlg::OnBnClickedButtongo()
{
m_bisGoing = true;
OnPaint();
if(m_fDiagramData.size() <= 0)
{
m_fDiagramData.push_back((float)rand() / RAND_MAX);
InvalidateRect(NULL, TRUE);
}
OnPaint();
for(int i(9); i >= 0; --i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
m_fDiagramData.push_back((float)rand() / RAND_MAX);
InvalidateRect(NULL, TRUE);
OnPaint();
}
m_bisGoing = false;
OnPaint();
}
void CKiUebung1Dlg::OnPaint()
{
if(IsIconic())
{
CPaintDC dc(this); // Gerätekontext zum Zeichnen
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// Symbol in Clientrechteck zentrieren
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Symbol zeichnen
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
{
constexpr const int border = 5;
CPaintDC dc(&m_cDiagram);
CRect l_cPos;
m_cDiagram.GetClientRect(&l_cPos);
const int width(l_cPos.Width() - border * 2 - 2), height(l_cPos.Height() - border * 2 - 12);
const int numPoints(m_fDiagramData.size());
POINT* points(new POINT[numPoints]);
for(int i(numPoints - 1); i >= 0; --i)
{
const int
x((float)i / (numPoints - 1) * width + border + 1),
y(height - m_fDiagramData[i] * height + border + 9);
points[i] = { x,y };
}
dc.Polyline(points, numPoints);
static CString going(_T(" "));
if(m_bisGoing) { going += _T("."); if(going.GetLength() > 300) going = _T(" ."); }
else going = _T(" ");
float fprog(0); if(m_fDiagramData.size() > 0) fprog = m_fDiagramData.back();
CString prog; prog.Format(_T("Progress %03.2f%%"), fprog * 100); if(m_bisGoing) prog += going;
m_cDiagram.SetWindowTextW(prog);
m_cDiagram.RedrawWindow();
delete[] points;
}
}
This is how it looks when the loop isn't running:
This is how it looks when the loop is running:
You seem to have trouble understanding how invalidating/painting works.
The documentation you should read first is:
Painting and Drawing
While many developers recommend painting only in WM_PAINT processing (OnPaint() in MFC), this is not always the best solution, because this message is low-priority, painting may not be immediate (have a "choppy" feel), and you may get a "flickering" effect.
Instead, I sometimes recommend a mix of drawing and painting:
Employ painting in WM_PAINT processing. This should paint the whole client area (or only the invalidated part of it, if you want a more "optimized" implementation). Please note that WM_PAINT message may be received as a result of invalidating a part or all of the client area, due to moving, resizing, unhiding etc the window, in addition to programmatically invalidating it. So in response to a WM_PAINT message you should perform a full repaint, ie all the items you want to be displayed.
Employ drawing for the changes you want to be shown immediately, while the application is busy (not waiting for the "asynchronous" WM_PAINT message to be received). Please note that these should be in WM_PAINT processing as well, so you rather have to write some drawing/painting routines, taking a HDC (or CDC*) as a parameter (along any other parameter needed), and call them from both the OnPaint() function (passing the ClientDC there) and from your additional drawing actions needed (passing a CDC* acquired by calling GetDC()).
So, let me share my experience with an application I wrote some (long) time ago. It's an image-display/manipulation (among others) application, processing images in a custom format, and using a special library, which was rather "slow", as it only provided a function to display the image in the device context (this includes possible cropping, adjustments, resizing etc which are CPU-costly operations). Here is an image:
You can see the user performing a selection. The application has to display the image, and possibly the selection rectangle on top of it, and of course that's what OnPaint() does. An "easy" (albeit technically "correct") implementation would be to call Invalidate() or InvalidateRect() in response each mouse move message (while selecting). This would cause a full repaint (which is "OK"), but also suffer from performance problems, due to the slow image-library: if you also call UpdateWindow() after invalidating (requesting an immediate refresh) performance would be sluggish (having to reprocess/redisplay the image), if not, the refresh would just take place some (noticeable) time later. This was solved by employing drawign (not painting) in response to the WM_MOUSEMOVE message: no invalidating there, instead drawing just the selection rectangle (after restoring the part modified by the previous selection message - I only backup/restore the four sides of the frame, not the whole rectangle). As a result, the application is responsive and the operation smooth, despite the slow library, and shows the image and the selection correctly, even if you switch to another application and then back to it, while the selection is being tracked (dashed line).
Some notes and suggestion about your implementation (it has quite a few issues):
As other members have noted, you may not call OnPaint() yourself. Especially those calls after Invalidate() make absolutely no sense. Instead, call UpdateWindow(), if you want an immediate update.
Imo it is NOT OK to perform calculations within OnPaint(), and I mean those points calculations (although in your case the calculation is rather trivial). OnPaint() should just display the data calculated in another part of your code.
Also, setting the m_cDiagram text and repainting from within OnPaint() is not OK either (may cause additional paint requests). Better move these into OnBnClickedButtongo().
You don't need to invalidate (and particularly erase) the whole client area to cause some controls to be repainted, instead invalidate only those controls. Remember, the sleep_for() function is blocking, and the WM_PAINT message won't be sent and processed while your loop is running.
Btw, consider a non-blocking approach, eg using a timer, as #Barmak Shemirani suggested. Alternatively, it may be possible to write a "non-blocing sleep()" by running the message-loop yourself (take parts of the code in CWinApp::Run() and modify it).
Since you have a dialog and created separate controls to display your data, using OnPaint() is not a good implementation, as it affects (paints) the whole client area. It is mostly useful for classes like CView or CScrollView (or custom-painting CWnds in general). You paint the graph on the dialog's surface, and have to perform calculations to get the coordinates in m_cDiagram (btw you can use GetWindowRect() and then ScreenToClient() instead) but it would be best to use an owner-drawn control (to paint/draw the graph on), and it's not really difficult, you just have to respond to paint requests (just as in OnPaint()), and the device context you get can paint on the control only, not on the dialog; coordinates are relative to the control's client area, starting from (0,0).
Hope this helps
CWnd::OnPaint is a response to WM_PAINT message and should not be called directly.
WM_PAINT calls CWnd::OnPaint, which calls CPaintDC dc(this), which in turns calls BeginPaint/EndPaint APIs. This sequence of message+response should be left as is.
Therefore CPaintDC dc(this) must appear once - and only once - inside OnPaint, and not anywhere else. Override OnPaint as follows:
void CMyDialog::OnPaint()
{
CDialogEx::OnPaint(); //this will call CPaintDC dc(this);
//optional:
CClientDC dc(this); //CClientDC can be used anywhere in a valid window
//use dc for drawing
}
//or
void CMyDialog::OnPaint()
{
CPaintDC dc(this);
//use dc for drawing
}
You also don't need the outdated if (IsIconic()) {...} condition.
To force the window to repaint itself, call Invalidate() (same thing as InvalidateRect(NULL, TRUE))
InvalidateRect(NULL, TRUE) is a request to repaint the window. The system will look at this request, and will send WM_PAINT message to that window when there is a chance. Therefore a call to InvalidateRect may not process the way you expect it to work in a sequential program. For example, a second consecutive call to InvalidateRect will not have any effect. Window was already marked to be updated.
for(int i(9); i >= 0; --i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
m_fDiagramData.push_back((float)rand() / RAND_MAX);
InvalidateRect(NULL, TRUE);
OnPaint();
}
OnPaint() should be removed from above code. Still, animation is not possible in a single thread (at least not in this manner). The program is busy going through the loop, it cannot deal with WM_PAINT and other messages.
So you need an additional thread, or simply use SetTimer, and respond to ON_WM_TIMER()/OnTimer for animation. Example:
int counter = 0;
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
ON_WM_PAINT()
ON_WM_TIMER()
...
END_MESSAGE_MAP()
void CMyDialog::OnPaint()
{
CPaintDC dc(this);
CString s;
s.Format(L"%02d", counter);
dc.TextOut(0, 0, s);
}
void CMyDialog::animate()
{
counter = 0;
SetTimer(1, 1000, NULL);
}
void CMyDialog::OnTimer(UINT_PTR n)
{
if(n == 1)
{
Invalidate(); //force repaint
counter++;
if(counter == 10)
KillTimer(1);
}
}

Can I change the image displayed on screen with a slider?

I want to use the value output by this slider to change the image which is loaded into the img variable. The paint function is called whenever the slider is moved.
The slider's value is 0 - 2. The second if statement tests as correct when the slider is > 1, however, the image does not change. Without the if statements I can switch the image manually, otherwise it isn't doing what I expect.
void ModelGUI::sliderValueChanged(Slider* slider)
{
rotate.getValue();
DBG(rotate.getValue());
}
void ModelGUI::paint (Graphics& g)
{
DBG("PAINT");
g.setColour(Colours::red);
if(rotate.getValue() < 1)
{
img = ImageFileFormat::loadFrom(f);
}
if(rotate.getValue() > 1)
{
img = ImageFileFormat::loadFrom(f2);
}
g.drawImage(img, model);
g.drawEllipse(225, 230, 2, 2, 2);
g.drawEllipse(240, 240, 2, 2, 2);
g.drawEllipse(245, 275, 2, 2, 2);
}
Why is this happening? Cheers
just a wild guess but shouldn't you force repainting in the sliderValueChanged event somehow? I do not know juce but I would expect one of these:
g.Repaint();
g.Refresh();
g.Update();
as changing slider is probably just repainting the slider itself and not updating the rest of your GUI. The g should be your Graphics object of coarse.
If the repaint is really time consuming then I usually do something like this:
bool _redraw=false; // some global variable (or local but accessible to your GUI/Form/Canvas whatever)
void any_event()
{
// here event stuff code
if (need_to_repaint) _redraw=true; // for example you change something hit a special key that changes something etc ...
}
void timer() // any timer with target max fps interval
{
if (_redraw())
{
_redraw=false;
// here force repaint or call the repaint directly
}
}
This way the repaint will not slow down other events too much. For example If I use zoom on wheel and I am rendering a lot of stuff the wheel could feel slagish if repainted on each change of wheel. This will repaint only with timer period which should be fine if set right.
The usual events that need such handling are zoom, mouse move/edit and window/view resize as they can create a lot of event calls per second ...
As #Spektre surmises -- in JUCE, your component needs to request that it be repainted when it knows that's necessary. In this case:
void ModelGUI::sliderValueChanged(Slider* slider)
{
rotate.getValue();
DBG(rotate.getValue());
repaint();
}
...will cause your ModelGUI component (and all of its child components) to repaint at the next reasonable point in time.
See: https://docs.juce.com/master/classComponent.html#af3ab3eabec93dd60faf983370f3a1206

SDL mousewheel and mousemotion

I made a previous question regarding mouse input which helped me understand mouse events worked for a start. I created a class that handles all kinds of input, and suffice it to say there are a few bugs which I've tried correcting with booleans but to no avail.
Mouse wheel and mousemotion events execute continuously even when the mouse wheel or mouse is not scrolling or moving.
I test for mouse scrolls with this simple call:
bool isMouseWheelScrolled() const
{
return pMouseWheelScrolled;
}
Then I listen for mouse events using a switch statement:
while(SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_MOUSEMOTION:
onMouseMove(event);
break;
case SDL_MOUSEWHEEL:
onMouseWheelScroll(event);
break;
default:
break;
}
}
void onMouseWheelScroll(SDL_Event &event)
{
switch(event.wheel.type)
{
case SDL_MOUSEWHEEL:
pMouseWheelScrolled = true;
pMouseWheel.x = event.wheel.x;
pMouseWheel.y = event.wheel.y;
break;
default:
break;
}
}
The same thing applies in mouse movement, but:
void onMouseMove(SDL_Event &event)
{
pMouseMoved = true;
pMousePosition.x = event.motion.x;
pMousePosition.y = event.motion.y;
}
It does have a problem with the booleans; the conditions for setting them to true are tested, but I don't know how I can analyse a condition for these booleans to become false. My solution would be to dwell into listening for mouse states, however I do not know how to do this and tutorials are insufficient in explaining them. The documentation isn't that intuitive either. The reason I need booleans is so I can test whether the mouse has moved or scrolled OUTSIDE the class. I can already test for key presses and mouse button presses; but mouse wheel and mouse moved are another issue; primarily because I need to obtain
event.motion.x;
event.motion.y;
event.wheel.x;
event.wheel.y;
for my camera. And I am not prepared to pass camera into my class because that breaks OO.
tldr; mouse wheel and mouse moved are set to true, but how to disable them when the mouse wheel isn't scrolling or when the mouse isn't moving?
I think you misunderstand how mouse events work. You do not get a SDL_MOUSEMOTION event while the mouse is moving. Instead you get a SDL_MOUSEMOTION event when the mouse is moved. That is, the event represents a single instant in time.
If you want to do something when the mouse is moved, your best option is to do it as direct response to the mouse event. If that goes against your OO design, then your design is wrong.‎‎‎‎
If you insist in using booleans, then you should set the boolean variable to false as soon as your consumer class sees it as true, so that each event is processed only once.
Anyway, my advice is to use some kind of interface, such as this one:
class IMouseListener
{
virtual OnMouseMove(int x, int y) =0;
virtual OnMouseWheel(int x, int y) =0;
};
Then make your main class implement this interface, and make your mouse handling code receive a pointer to such interface.

Draw new line every time mouse is moved in c++ builder

I have a code I want to load every time I move my mouse cursor; it draws line to direction of mouse and beyond, and every time it the line gets to certain x coordinate, the line reflects. The problem is that now the program only draws line when I click on the PaintBox.
Here is my code so far:
void __fastcall TForm2::PaintBox1Click(TObject *Sender)
{
Form2->Refresh();
TPoint P;
::GetCursorPos( &P );
P = ScreenToClient( P );
int XX;
int YY;
if (P.x<240)
{
XX=15;
YY= ((445-P.y)*(XX-P.x)/(240-P.x)+P.y);
}
else if(P.x==240)
{
XX=240;YY=-5;
}
else
{
XX=465;
YY= ((445-P.y)*(XX-P.x)/(240-P.x)+P.y);
}
int delta=2*(445-YY);
this->Canvas->MoveTo(240, 445);
this->Canvas->LineTo(XX,YY);
while(0<YY&&YY<480&&YY!=445)
{
XX=abs(480-XX);
YY-=delta;
this->Canvas->LineTo(XX,YY);
}
}
You are trying to paint in an OnClick event handler. That's the wrong place to paint. The only correct place to paint to a VCL paint box is from its OnPaint event handler.
You will need to respond to OnClick though. Make a note of the location of the click and update any state that you need to maintain. Then call Invalidate on the paint box to force a paint cycle.