How to draw a transparent rectangle over an existing drawing? Topic is solved

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
Post Reply
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Thu Aug 05, 2021 5:15 am

I have a wxPanel over which I am drawing a bitmap and another drawing a line on top of it, I also want to draw a transparent rectangle like the one in the drawing sample that comes with the source. Basically I want to be able to draw a rectangle based on the what mouse position I Ctrl+LMB click, hold and drag to whatever mouse position, and instead of showing a message box like in the sample program, I want to turn it into a selected, another rectangle basically, which is transparent and can show the bitmap behind it.
Like when I draw a rectangle over the bitmap,
Image

and after the left button is up it turns it into something like this,
Image

representing a selected area.

I have tried doing this,
In the mouse motion handler,

Code: Select all

    if (event.ControlDown())
    {
        // int x,y;
        // event.GetPosition(&x,&y);
        // wxScrolledWindow sw(this);
        // sw.CalcUnscrolledPosition(x, y, &xx, &yy);
        m_CurrentPoint = wxPoint(pos.x , pos.y) ;
        wxRect rect (m_AnchorPoint, m_CurrentPoint) ;

        wxClientDC dc(this) ;
        PrepareDC(dc) ;
        wxDCOverlay overlaydc(m_Overlay, &dc);
        overlaydc.Clear();
        dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
        dc.SetBrush(*wxTRANSPARENT_BRUSH);
        dc.DrawRectangle(rect);
    }
in the mouse left down handler,

Code: Select all

    if (event.ControlDown())
    {
        // int x,y;
        // event.GetPosition(&x,&y);
        // wxScrolledWindow sw(this);
        // sw.CalcUnscrolledPosition(x, y, &xx, &yy);
        // m_AnchorPoint = wxPoint(xx, yy) ;
        m_CurrentPoint = wxPoint(pos.x, pos.y);
        // m_rubberBand = true ;
        CaptureMouse();
        SetCursor(wxCURSOR_CLOSED_HAND);
        wxLogDebug("Control pressed");
    }
    else
    {
        SetCursor(wxCURSOR_ARROW);
        return;
    }
and in the mouse left up handler,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("Control pressed");

        ReleaseMouse();
        {
            wxClientDC dc(this);
            PrepareDC(dc);
            wxDCOverlay overlaydc(m_Overlay, &dc);
            overlaydc.Clear();
        }
        m_Overlay.Reset();
        // m_rubberBand = false;

        // wxScrolledWindow sw;
        // wxPoint endpoint = sw.CalcUnscrolledPosition(event.GetPosition());

        // Don't pop up the message box if nothing was actually selected.
        // if ( endpoint != m_AnchorPoint )
        // {
        //     wxLogMessage("Selected rectangle from (%d, %d) to (%d, %d)",
        //                  m_AnchorPoint.x, m_AnchorPoint.y,
        //                  endpoint.x, endpoint.y);

        m_AnchorPoint = m_CurrentPoint;
        m_CurrentPoint = wxPoint(pos.x, 0);

        DrawSelectionArea(m_AnchorPoint.x, m_CurrentPoint.y, m_CurrentPoint.x - m_AnchorPoint.x, this->GetSize().GetHeight());
        // }
    }
And this is the DrawSelectionArea()

Code: Select all

void WaveformViewer::DrawSelectionArea(int xx, int yy, int ww, int hh)
{
    wxClientDC dc(this);

    dc.SetPen(wxPen(wxColour(200, 200, 200, 100), 2, wxPENSTYLE_SOLID));
    // dc.SetPen(*wxTRANSPARENT_PEN);
    // dc.SetBrush(*wxTRANSPARENT_BRUSH);
    dc.DrawRectangle(wxRect(xx, yy, ww, hh));
}
It looks like this currently,
Image

The code is incomplete and some calculations are wrong as I copied the code from the sample drawing program, however it uses a method CalcUnscrolledPosition() which a method of wxScrolledWindow, but I am drawing on wxPanel, what should be the equivalent of this method for wxPanel?

Also it starts drawing the rectangle as soon as I press CTRL but I only want to drawn when CTRL+LMB is pressed, and stop at the position where LMB is up.

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Thu Aug 05, 2021 8:28 am

however it uses a method CalcUnscrolledPosition() which a method of wxScrolledWindow, but I am drawing on wxPanel, what should be the equivalent of this method for wxPanel?
You can just remove it, you don't need it in a wxPanel.
Also it starts drawing the rectangle as soon as I press CTRL but I only want to drawn when CTRL+LMB is pressed, and stop at the position where LMB is up.
You can use event.LeftIsDown() in the motion handler.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Thu Aug 05, 2021 11:29 am

You can use event.LeftIsDown() in the motion handler.
This seems to fix the problem, but for testing I had the wxTimer set to not start i.e commented out, in one of other drawing, when I uncomment the Start() the rectangle I draw for selection keep appearing and disappearing, probably because the panel is being updated.
Image

Also after I lift the left button up, it doesn't draw the other rectangle I had the DrawSelectionArea() for,

Here is the mouse motion handler,

Code: Select all

    if (event.ControlDown() && event.LeftIsDown())
    {
        m_CurrentPoint = wxPoint(pos.x , pos.y);
        wxRect rect(m_CurrentPoint, m_AnchorPoint);

        wxClientDC dc(this);
        PrepareDC(dc);

        wxDCOverlay overlaydc(m_Overlay, &dc);
        overlaydc.Clear();

        dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
        dc.SetBrush(*wxTRANSPARENT_BRUSH);

        dc.DrawRectangle(rect);
    }
}
Here is the mouse left down handler,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("Control pressed");

        m_AnchorPoint = wxPoint(pos.x, pos.y);
        m_CurrentPoint = m_AnchorPoint;

        CaptureMouse();                  // <------ I have a CaptureMouse() here..
        SetCursor(wxCURSOR_CLOSED_HAND);
    }
    else
    {
        SetCursor(wxCURSOR_ARROW);
        return;
    }
}
Here is the mouse left up handler,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("Control pressed");

        // ReleaseMouse();        // <------- But uncommenting this gives error
        {
            wxClientDC dc(this);
            PrepareDC(dc);
            wxDCOverlay overlaydc(m_Overlay, &dc);
            overlaydc.Clear();
        }
        m_Overlay.Reset();

        m_AnchorPoint = m_CurrentPoint;
        m_CurrentPoint = wxPoint(pos.x, pos.y);

        DrawSelectionArea(m_AnchorPoint.x, m_CurrentPoint.y, m_CurrentPoint.x - m_AnchorPoint.x, this->GetSize().GetHeight());   // <---- This doesn't get drawn
    }
    else
    {
        m_MediaCtrl.Seek(seek_to, wxFromStart);
        m_StatusBar.PushStatusText(wxString::Format(_("Now playing: %s"), selected), 1);
        m_MediaCtrl.Play();
    }
}
Also gives me error when I release the mouse, even though I have a CaptureMouse() in the left down handler.

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Thu Aug 05, 2021 12:16 pm

... the rectangle I draw for selection keep appearing and disappearing, probably because the panel is being updated.
Also after I lift the left button up, it doesn't draw the other rectangle I had the DrawSelectionArea() for,
This kind of stuff happens when you're using wxClientDC, because the state you're drawing in the paint event handler is not in sync with what's on screen.

Solution: Don't use wxClientDC and wxOverlay. In the mouse handler(s), modify a state and draw everything in the paint event handler.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Thu Aug 05, 2021 2:04 pm

This kind of stuff happens when you're using wxClientDC, because the state you're drawing in the paint event handler is not in sync with what's on screen.

Solution: Don't use wxClientDC and wxOverlay. In the mouse handler(s), modify a state and draw everything in the paint event handler.
I see, I modified the code as you mentioned, I made 3 booleans, and in mouse motion handler, I have,

Code: Select all

    if (event.ControlDown() && event.LeftIsDown())
    {
        m_CurrentPoint = wxPoint(pos.x , pos.y);

        bMouseMotion = true;
    }
mouse left down handler,

Code: Select all

    if (event.ControlDown())
    {
        m_AnchorPoint = wxPoint(pos.x, pos.y);
        m_CurrentPoint = m_AnchorPoint;

        bMouseLDown = true;
    }
mouse left down handler,

Code: Select all

    if (event.ControlDown())
    {
        m_AnchorPoint = m_CurrentPoint;
        m_CurrentPoint = wxPoint(pos.x, pos.y);

        bMouseLUp = true;
    }
and OnPaint(),

Code: Select all

    if (bMouseMotion)
    {
        wxRect rect(m_CurrentPoint, m_AnchorPoint);

        dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
        dc.SetBrush(*wxTRANSPARENT_BRUSH);

        dc.DrawRectangle(rect);
    }
    else if (bMouseLDown)
    {
        wxLogDebug("Control pressed");

        // CaptureMouse();
        SetCursor(wxCURSOR_CLOSED_HAND);
    }
    else if (bMouseLUp)
    {
        wxLogDebug("Control pressed");

        // ReleaseMouse();
        DrawSelectionArea(m_AnchorPoint.x, m_CurrentPoint.y, m_CurrentPoint.x - m_AnchorPoint.x, this->GetSize().GetHeight());
    }
Its much better now, however it doesn't clean the rectangle that I draw with mouse and doesn't draw the other rectangle in the DrawSelectionArea(),

Code: Select all

void WaveformViewer::DrawSelectionArea(int xx, int yy, int ww, int hh)
{
    wxClientDC dc(this);

    dc.SetPen(wxPen(wxColour(200, 200, 200, 100), 2, wxPENSTYLE_SOLID));
    // dc.SetPen(*wxTRANSPARENT_PEN);
    // dc.SetBrush(*wxTRANSPARENT_BRUSH);
    dc.DrawRectangle(wxRect(xx, yy, ww, hh));
}
Image

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Thu Aug 05, 2021 6:54 pm

Why are you still using wxClientDC? All drawing should happen in the paint event handler.

Also, i think there's too much "low-level" information used in the paint event handler. It shouldn't have to deal with things like mouse-button pressed or not. It only needs the information relevant for drawing:
- is a part of the waveform selected and if yes, what range?
- should a selection rectangle be drawn? if yes, which coordinates?

Also, things like SetCursor(wxCURSOR_CLOSED_HAND) do not belong in a paint event handler.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Thu Aug 05, 2021 9:42 pm

doublemax wrote:
Thu Aug 05, 2021 6:54 pm
Why are you still using wxClientDC? All drawing should happen in the paint event handler.

Also, i think there's too much "low-level" information used in the paint event handler. It shouldn't have to deal with things like mouse-button pressed or not. It only needs the information relevant for drawing:
- is a part of the waveform selected and if yes, what range?
- should a selection rectangle be drawn? if yes, which coordinates?

Also, things like SetCursor(wxCURSOR_CLOSED_HAND) do not belong in a paint event handler.
Ok, I made some changes,

OnPaint()

Code: Select all

    if (bSelectWaveformRange)
    {
        wxRect rect(m_CurrentPoint, m_AnchorPoint);

        dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
        dc.SetBrush(*wxTRANSPARENT_BRUSH);

        dc.DrawRectangle(rect);

        // bSelectWaveformRange = false;

        if (bWaveformRangeSelected)
        {
            // dc.Clear();
            dc.SetPen(wxPen(wxColour(255, 100, 255, 255), 4, wxPENSTYLE_SOLID));
            // dc.SetPen(*wxTRANSPARENT_PEN);
            dc.SetBrush(*wxTRANSPARENT_BRUSH);
            dc.DrawRectangle(wxRect(m_AnchorPoint.x, 0, m_CurrentPoint.x - m_AnchorPoint.x, this->GetSize().GetHeight()));
        }
    }
}
mouse motion,

Code: Select all

    if (event.ControlDown() && event.LeftIsDown())
    {
        m_CurrentPoint = wxPoint(pos.x , pos.y);

        bSelectWaveformRange = true;
    }
mouse left down,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("Control pressed");

        m_AnchorPoint = wxPoint(pos.x, pos.y);
        m_CurrentPoint = m_AnchorPoint;

        CaptureMouse();
        SetCursor(wxCURSOR_CLOSED_HAND);
    }
mouse left up,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("Control pressed");

        m_AnchorPoint = m_CurrentPoint;
        m_CurrentPoint = wxPoint(pos.x, pos.y);

        bSelectWaveformRange = false;
        bWaveformRangeSelected = true;
    }
Now it does draw the selection area, but not when I lift left up, it draws when I drag, lift left up and then again when I press left down, then it draws, and the drawing doesn't stay on screen if I lift left up before CTRL, I have to keep left down, lift CTRL, then lift left up, and the selection rectangle(grey/white one) does not go away.
Image

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Thu Aug 05, 2021 10:18 pm

Do you still have the timer running that constantly refreshes the panel? If not you'll have to call Refresh() when the drag operation ends, so that the rubberband can get overdrawn.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Thu Aug 05, 2021 11:17 pm

doublemax wrote:
Thu Aug 05, 2021 10:18 pm
Do you still have the timer running that constantly refreshes the panel? If not you'll have to call Refresh() when the drag operation ends, so that the rubberband can get overdrawn.
Yes, I have the wxTimer active.

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Fri Aug 06, 2021 6:01 am

There must be a simple logical error in the flag handling, but without being able to test it myself, it's getting harder and harder to follow the code.

Just put a bunch of debug output in all crucial parts of the mouse handler, maybe you can spot the error.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Fri Aug 06, 2021 11:06 am

doublemax wrote:
Fri Aug 06, 2021 6:01 am
There must be a simple logical error in the flag handling, but without being able to test it myself, it's getting harder and harder to follow the code.

Just put a bunch of debug output in all crucial parts of the mouse handler, maybe you can spot the error.
Ok I made some progress, but it is still not properly working, I firstly changed the boolean variable names, I think they were confusing me, now my OnPaint() looks like this,

Code: Select all

    if (bSelectRange)
    {
        wxRect rect(m_CurrentPoint, m_AnchorPoint);

        dc.SetPen(wxPen(wxColour(200, 200, 200), 2, wxPENSTYLE_SOLID));
        dc.SetBrush(wxBrush(wxColour(200, 200, 200, 80), wxBRUSHSTYLE_SOLID));
        dc.DrawRectangle(rect);
    }

    if (!bSelectRange && bDrawSelectedArea)
    {
        dc.SetPen(wxPen(wxColour(255, 100, 255, 255), 4, wxPENSTYLE_SOLID));
        dc.SetBrush(wxBrush(wxColour(200, 200, 200, 80), wxBRUSHSTYLE_SOLID));
        dc.DrawRectangle(wxRect(m_AnchorPoint.x, 0, m_CurrentPoint.x - m_AnchorPoint.x, this->GetSize().GetHeight()));
    }
}
Mouse motion,

Code: Select all

    if (event.ControlDown())
    {
        m_CurrentPoint = wxPoint(pos.x , pos.y);

        wxLogDebug("CTRL pressed, pressing LMB will draw selection range at %d, %d", pos.x, pos.y);
    }
Mouse left down,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("LMB pressed");

        bSelectRange = true;

        m_AnchorPoint = wxPoint(pos.x, pos.y);
        m_CurrentPoint = m_AnchorPoint;

        CaptureMouse();
        SetCursor(wxCURSOR_CLOSED_HAND);
    }
    else
    {
        SetCursor(wxCURSOR_ARROW);
        return;
    }
Mouse left up,

Code: Select all

    if (event.ControlDown())
    {
        wxLogDebug("LMB released");

        m_AnchorPoint = m_CurrentPoint;
        m_CurrentPoint = wxPoint(pos.x, pos.y);

        bSelectRange = false;

        if (!bSelectRange)
            bDrawSelectedArea = true;
    }
    else
    {
        m_MediaCtrl.Seek(seek_to, wxFromStart);
        m_StatusBar.PushStatusText(wxString::Format(_("Now playing: %s"), selected), 1);
        m_MediaCtrl.Play();
    }
If I understand correctly, the mouse motion only needs to start calculating, mouse left down needs to start drawing, and mouse left up needs to stop calculating and drawing, and draw the region rectangle.

Currently it looks like this,
Image

It should draw the region when the LMB is up regardless of weather the CTRL key is up or not.

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Fri Aug 06, 2021 11:38 am

Motion handler: I think if (event.ControlDown()) should be if (bSelectRange)
As you don't return inside the if-clause, what else is happening in the motion handler?

Mouse up handler: Same as before: if (event.ControlDown()) should be if (bSelectRange)

BTW: What shall happen when the user releases the control key while drawing the selection rectangle? I assume the selection should stop without changing any previous selection. That special case needs to be dealt with in the motion handler.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Fri Aug 06, 2021 12:41 pm

doublemax wrote: Motion handler: I think if (event.ControlDown()) should be if (bSelectRange)
As you don't return inside the if-clause, what else is happening in the motion handler?

Mouse up handler: Same as before: if (event.ControlDown()) should be if (bSelectRange)
Here is the complete mouse motion handler,

Code: Select all

void WaveformViewer::OnMouseMotion(wxMouseEvent& event)
{
    Database db(m_InfoBar);

    int selected_row = m_Library.GetSelectedRow();
    if (selected_row < 0)
        return;

    wxString selected = m_Library.GetTextValue(selected_row, 1);
    std::string path = db.GetSamplePathByFilename(m_DatabaseFilepath, selected.BeforeLast('.').ToStdString());

    Tags tags(path);

    int length = tags.GetAudioInfo().length;
    double position = m_MediaCtrl.Tell();

    int panel_width = this->GetSize().GetWidth();
    double line_pos = panel_width * (position / length);

    wxPoint pos = event.GetPosition();

    double seek_to = ((double)pos.x / panel_width) * length;

    if (abs(pos.x - line_pos) <= 5 && pos.y <= 5)
    {
        SetCursor(wxCursor(wxCURSOR_HAND));
        wxLogDebug("Cursor on playhead..");
    }
    else
    {
        SetCursor(wxCURSOR_ARROW);
        return;
        // wxLogDebug("Mouse at: '(%d, %d)'", pos.x, pos.y);
    }

    if (bSelectRange)
    {
        m_CurrentPoint = wxPoint(pos.x , pos.y);

        wxLogDebug("CTRL pressed, pressing LMB will draw selection range at %d, %d", pos.x, pos.y);
    }

    if (event.ControlDown())
    {
        SetCursor(wxCURSOR_IBEAM);
    }
    else
    {
        SetCursor(wxCURSOR_ARROW);
        return;
    }
}
I also added these 2 print statements in the OnPaint(),

Code: Select all

    if (bSelectRange)
    {
        wxLogDebug("Drawing select range");
    .
    .
    }
    if (!bSelectRange && bDrawSelectedArea)
    {
        wxLogDebug("Drawing selected area");
    .
    .
    }
Now it draws the selected area, but is not drawing the selection range while mouse is being dragged,
Image
doublemax wrote: BTW: What shall happen when the user releases the control key while drawing the selection rectangle? I assume the selection should stop without changing any previous selection. That special case needs to be dealt with in the motion handler.
Yes if the control key is up while drawing it should cancel the action and nothing should happen. I added a check in mouse motion I shared above as well.

User avatar
doublemax
Moderator
Moderator
Posts: 16383
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to draw a transparent rectangle over an existing drawing?

Post by doublemax » Fri Aug 06, 2021 1:00 pm

The only error is see is that the selection rectangle isn't drawn. Did you check the coordinates and pen colors?

BTW: I think there should be separate variables for selection rectangle and selection range.
Use the source, Luke!

apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 360
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to draw a transparent rectangle over an existing drawing?

Post by apoorv569 » Fri Aug 06, 2021 3:05 pm

doublemax wrote: The only error is see is that the selection rectangle isn't drawn. Did you check the coordinates and pen colors?
I didn't change any coordinates or pen colors? Just the conditions as mentioned previously.
doublemax wrote: BTW: I think there should be separate variables for selection rectangle and selection range.
They are separate variables as you can see in the OnPaint(), I'm using bSelectRange for drawing selection rectangle, and bDrawSelectionArea for drawing the selected area.

But I think because I have removed the if (event.ControlDown()) from the mouse motion handler, the calculation for coordinates is changed now, because now it starts in the mouse left down handler instead (?).

Post Reply