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.
Related
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);
}
}
I'm trying to make some animation with SDL2.
SDL2 has some bad performance, and my simple chess game was running at 10fps.
I don't want to redraw the whole screen every frames, it takes way to long. So in order to optimize my game I decided to code an Animation class ( a simple fade in/out effect) which only redraw some part of the screen everyframe (basicly redraw the selected piece)
void myGame::sdlLoop() {
SDL_Event events;
bool quit = false;
while (!quit) {
bool redraw = false; //We assume we don't want to redraw everything (yet)
while (SDL_PollEvent(&events)) {
switch (events.key.keysym.scancode) {
....
}
if(redraw) draw(); // Only redraw whole screen IF NEEDED
else drawAnimations(); // Better use that for better performance
}
}
}
void myGame::drawAnimations(){
int i = 0;
while(i < arr.size()){
....
drawThingsAtCertainsPixels(now_time, animation_start_time, animation_duration); //Basicly a simple fade effect, something like
//pixelColor = color1 * advancement + color2 * (1 - advancement)
}
// Show the window
SDL_RenderPresent( m_renderer );
}
So far so good, but I noticed a weird behavior.
The animation is "jerky", most of the frames are skipped
I ended up avec all of my fadeout unfinished because the last frame where skipped.
But, when I constantly move the mouse, everything goes right and no frame are dropped
I think it is linked to SDL wanting to optimize performance, and only run at 100% speed when someting important is going on (user inputting things or windows interacted).
Do you know why is this happening, and how to fix that ?
I mean how to have SDL2 computing every frame even if I don't move the mouse.
I feel a bit guilty for finding the solution right after posting this question (but I swear I tore my hair out a lot)
The solution is quite simple
while (!quit) {
bool redraw = false; //We assume we don't want to redraw everything (yet)
while (SDL_PollEvent(&events)) {
switch (events.key.keysym.scancode) {
....
}
if(redraw) draw(); // Only redraw whole screen IF NEEDED
else drawAnimation(); // Better use that for better performance
}
}
Just move the drawAnimations() function a bit lower, out of the while (SDL_PollEvent(&events)) {
while (!quit) {
bool redraw = false; //We assume we don't want to redraw everything (yet)
while (SDL_PollEvent(&events)) {
switch (events.key.keysym.scancode) {
....
}
}
if(redraw) draw(); // Only redraw whole screen IF NEEDED
else drawAnimation(); // Better use that for better performance
}
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;
}
I move sprites stored in a formationvector by holding the mousebutton. Problem is: Whenever i move the sprite onto another one, i move both sprites at the same time.
What i want: Moving the sprite1 over the other sprites2 without changing the position of sprite 2 or with other words:
-Testing in a loop if a sprite of a vector is clicked on
-If sprite is clicked on:
Move this sprite around while button is pressed but somehow stop this move while this movement in order to avoid moving more than this one sprite.
Here is my try so far:
while (App.pollEvent(Event))
{
// Window closed
if (Event.type == sf::Event::Closed)
{
return (-1);
}
if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
{
for (size_t k = 0; k < formation.size(); k++)
{
if (isMouseOver(formation[k], App) == true)
{
Mouseposition = sf::Vector2f(sf::Mouse::getPosition(App));
Mouseposition.x = Mouseposition.x - formation[k].getLocalBounds().width / 2;
Mouseposition.y = Mouseposition.y - formation[k].getLocalBounds().height / 2;
formation[k].setPosition(sf::Vector2f(Mouseposition));
Formation_playernames.clear();
Formation_playerinformation.clear();
Formation_Playernames(Font, Formation_playernames, formation, playerlist);
Formation_Playerinformation(Font, Formation_playerinformation, formation, playerlist);
}
}
}
}
The Formation-functions store the correct new positions of the sprites and their colors based on the positions into the formation vector
The problem is the for-loop, i could do it without but that would lead to a much longer code. How could i do it ?
Instead of simply checking whether or not the mouse button is pressed, you should structure your code so you can check exactly when the button is pressed, and when it is released. You can then structure your code such that different presses of the button are distinguished from each other. semi-pseudocode follows
bool button_is_pressed = false;
Sprite* currently_selected_sprite = nullptr;
// main application loop
while (...)
{
...
// other application logic
...
if (!button_is_pressed)
{
if (CheckIfButtonIsPressed())
{
button_is_pressed = true;
// Button was just pressed.
// Select the appropriate sprite by checking
// the mouse coordinates against the positions
// of the sprites.
}
else
{
// Button not being pressed.
// Likely no logic needed here.
}
}
else // button_is_pressed == true
{
if (CheckIfButtonIsPressed())
{
// Button is being held down.
// Implement dragging logic using the
// pointer to the selected sprite.
}
else
{
button_is_pressed = false;
// Button was just released.
// Deselect the sprite.
currently_selected_sprite = nullptr;
}
}
}
Alternatively, you could handle mouse events, which will take care of much of this logic for you. http://sfml-dev.org/documentation/2.0/classsf_1_1Event.php
In a more English like pseudo code, this is what your function is doing:
if the mouse button is currently pressed
move all the sprites which are under the mouse to be centered on the mouse cursor
else
do nothing
This is what I'm suggesting
at the moment the mouse button is pressed down
select the sprite which is under the mouse cursor
at the moment the mouse button is released
deselect the selected sprite
if the mouse button is currently pressed, and a sprite is selected
move the selected sprite to the position of the mouse cursor
i am new to opengl.
i want to draw my scene a few times when the user presses some key, i have called glutPostRedisplay in a for loop when the key is pressed but it just redraws my scene one time. how should i handle this problem?
First off, glutPostRedisplay is a function that belongs to GLUT, which is not part of OpenGL but a 3rd party library/framework. There are other frameworks, or you can do everything from scratch (heck, I just remembered, that when I began learning OpenGL some 14 years ago, GLUT won't properly work for me, so I did write my framework from scratch then).
i want to draw my scene a few times when the user presses some key
Never mix drawing and animation logic with event processing. If the user pressed a key that triggers some animation, set a flag (=some variable), that the animation should be played and then iterate through the animation in your render/animation loop.
i have called glutPostRedisplay in a for loop when the key is pressed but it just redraws my scene one time
glutPostRedisplay won't trigger a redraw immediately. It sets a flags, that the GLUT message loop shouls issue a redraw instead of going idle after all message processing. Of course this flag doesn't accumulate.
So here's a layout using GLUT. Unfortunately GLUT is subobtimal for this kind of things, because it doesn't give you control over the event loop, which makes precise timing cumbersome to achieve.
time_t rendertimer;
void stopwatch(time_t); // some external helper function that reports time between calls
typedef enum {stop, play} animstate;
struct animation {
float time;
float duration;
animstate state;
};
animation animations[...];
void keyboard(key, x, y)
{
if(key == ...) {
animations[0].time = 0;
animations[0].state = play;
}
glutPostRedisplay();
}
void idle()
{
float deltaT = stopwatch(rendertimer);
if( animation[...].state == play ) {
animation[...].time += deltaT;
if( animation[...].duration <= animation[...].time ) {
animation[...].state = stop;
}
}
glutPostRedisplay();
}
void display()
{
draw_objects_according_to_animation();
}