How to remove trailing effect when drawing on a panel? 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.
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

I am trying to draw on a wxPanel. My application is for auditioning audio samples, and I'm trying to draw a playhead/seekbar like thing, to show the current position of the track. I took some code from https://wiki.wxwidgets.org/Drawing_on_a_panel_with_a_DC, it is working, however if a sample is very short, less than a second for example, the line needs to move very fast, and it creates a trailing like effect. How can I get rid of that effect, and make the transition smoother? I made a GIF to show example, its not that high quality, but hopefully shows the problem.
Image

The area that I'm drawing on, is m_TopWaveformPanel. Here is the relevant code,
In the constructor I bind the event like this,

Code: Select all

    Bind(wxEVT_PAINT, &MainFrame::PaintEvent, this);
Then I have these functions, that I took from the link I mentioned above,

Code: Select all

void MainFrame::PaintEvent(wxPaintEvent& evt)
{
    wxPaintDC dc(this);
    Render(dc);
}

void MainFrame::PaintNow()
{
    wxClientDC dc(this);
    Render(dc);
}

void MainFrame::Render(wxDC& dc)
{
    int selected_row = m_Library->GetSelectedRow();

    if (selected_row < 0)
        return;

    wxString selected = m_Library->GetTextValue(selected_row, 1);
    std::string path = GetFilenamePathAndExtension(selected).Path.ToStdString();

    Tags tags(path);

    m_TopWaveformPanel->SetDoubleBuffered(true);

    int length = tags.GetAudioInfo().length;
    wxLogDebug("Len: %d", length);

    double position = m_MediaCtrl->Tell();
    wxLogDebug("Pos: %f", position);

    dc.SetPen(wxPen(wxColor(255,0,0,255), 2, wxPENSTYLE_SOLID));

    m_Timer->Start(1, wxTIMER_CONTINUOUS);

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

    wxLogDebug("Drawing at: %f", line_pos);

    dc.DrawLine(line_pos, m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight() - 1),
                line_pos, m_TopWaveformPanel->GetSize().GetHeight() - 1);
}
Also it keeps refreshing/updating the drawing even after the sample stops playing, I assume probably because I have a wxTimer started, I don't know where to call the Stop(). I use this timer to also update the the elapsed time. Here is this relevant code,

Code: Select all

void MainFrame::UpdateElapsedTime(wxTimerEvent& event)
{
    wxString duration, position;
    wxLongLong llLength, llTell;

    llLength = m_MediaCtrl->Length();
    int total_min = static_cast<int>((llLength / 60000).GetValue());
    int total_sec = static_cast<int>(((llLength % 60000) / 1000).GetValue());

    llTell = m_MediaCtrl->Tell();
    int current_min = static_cast<int>((llTell / 60000).GetValue());
    int current_sec = static_cast<int>(((llTell % 60000) / 1000).GetValue());

    duration.Printf(wxT("%2i:%02i"), total_min, total_sec);
    position.Printf(wxT("%2i:%02i"), current_min, current_sec);

    m_SamplePosition->SetLabel(wxString::Format(wxT("%s/%s"), position.c_str(), duration.c_str()));

    // ==================================================
    ClearBackground();            <------- Line drawing timer starts here..
    PaintNow();
    Refresh();
}
For the elapsed time, I call the m_Timer->Stop() in event handler for wxEVT_MEDIA_FINISHED, is there a similar event for draw/paint.
ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 7459
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: How to remove trailing effect when drawing on a panel?

Post by ONEEYEMAN »

Hi,
Try to use wxAutoBufferedPaintedDC.

Thank you.
User avatar
doublemax
Moderator
Moderator
Posts: 19115
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to remove trailing effect when drawing on a panel?

Post by doublemax »

Get rid of the PaintNow() method. If you want to force a redraw, call just call Refresh().

Code: Select all

m_TopWaveformPanel->SetDoubleBuffered(true);
This is something you need to call only once, during construction and not in the paint event handler. But i also had cases where this causes redraw issue, so comment this out for now.

Code: Select all

ClearBackground();            <------- Line drawing timer starts here..
What does this do?

Code: Select all

void MainFrame::Render(wxDC& dc)
{
    int selected_row = m_Library->GetSelectedRow();

    if (selected_row < 0)
        return;
I see nothing that clears the background first.
Use the source, Luke!
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

doublemax wrote: Get rid of the PaintNow() method. If you want to force a redraw, call just call Refresh().
Removing PaintNow() fixed it. Thanks.
doublemax wrote:

Code: Select all

m_TopWaveformPanel->SetDoubleBuffered(true);
This is something you need to call only once, during construction and not in the paint event handler. But i also had cases where this causes redraw issue, so comment this out for now.
I see, I commented out this for now. Seems to not make any difference, at least I can't see any difference. But I will add it if needed in future.
doublemax wrote:

Code: Select all

ClearBackground();            <------- Line drawing timer starts here..
What does this do?
I was just experimenting with this one, Clear, Draw, Refresh method. Forgot to remove it.
ONEEYEMAN wrote: Try to use wxAutoBufferedPaintedDC.
This also works, thanks. What's the difference between this, and wxPaintDC?

Also I start wxTimer in he Render(), where do I call Stop() for the timer? As it keeps doing something even when the sample stops playing. Stop() does it gets called when the sample stops playing though, should that enough for all Start() calls?

Also I would like to move the drawn line, with mouse cursor, for using it as a seekbar. Is it possible to do so?
User avatar
doublemax
Moderator
Moderator
Posts: 19115
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to remove trailing effect when drawing on a panel?

Post by doublemax »

Code: Select all

This also works, thanks. What's the difference between this, and wxPaintDC?
A wxBufferedPaintDC uses a bitmap as buffer, all drawing commands are drawn onto the bitmap any only when it gets destroyed it blits the bitmap onto the actual wxPaintDC. This avoid flickering.

wxAutoBufferedPaintDC uses a normal wxPaintDC on platforms that use doublebuffering by default (e.g. OSX) and a wxBufferedPaintDC on others.
Use the source, Luke!
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

doublemax wrote: Thu Jul 29, 2021 8:30 am

Code: Select all

This also works, thanks. What's the difference between this, and wxPaintDC?
A wxBufferedPaintDC uses a bitmap as buffer, all drawing commands are drawn onto the bitmap any only when it gets destroyed it blits the bitmap onto the actual wxPaintDC. This avoid flickering.

wxAutoBufferedPaintDC uses a normal wxPaintDC on platforms that use doublebuffering by default (e.g. OSX) and a wxBufferedPaintDC on others.
I see. What about the ability to move the drawn line with mouse, to seek through the audio sample? Like how a wxSlider would work.
User avatar
doublemax
Moderator
Moderator
Posts: 19115
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to remove trailing effect when drawing on a panel?

Post by doublemax »

What about the ability to move the drawn line with mouse, to seek through the audio sample? Like how a wxSlider would work.
Possible, but you'll have to code the whole functionality.

- catch wxEVT_MOTION event on the panel
- if the mouse is near the line, change cursor to indicate that the user can grab it
- on mouse-down and if mouse is near the line, capture mouse ( wxWindow::CaptureMouse )
- while mouse is captured, move the position of the line to the position of the mouse pointer in the wxEVT_MOTION handler
- on mouse-up release mouse capture
Use the source, Luke!
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

doublemax wrote: Thu Jul 29, 2021 10:41 am
What about the ability to move the drawn line with mouse, to seek through the audio sample? Like how a wxSlider would work.
Possible, but you'll have to code the whole functionality.

- catch wxEVT_MOTION event on the panel
- if the mouse is near the line, change cursor to indicate that the user can grab it
- on mouse-down and if mouse is near the line, capture mouse ( wxWindow::CaptureMouse )
- while mouse is captured, move the position of the line to the position of the mouse pointer in the wxEVT_MOTION handler
- on mouse-up release mouse capture
Thanks, I think I do understand this. But when I capture wxEVT_MOTION, and I enter the panel, its not very fast in capturing position. It sometimes updates, sometimes doesn't.

I bind the event,

Code: Select all

    m_TopWaveformPanel->Bind(wxEVT_MOTION, &MainFrame::OnGrabPlayhead, this);
and this is the handler

Code: Select all

void MainFrame::OnGrabPlayhead(wxMouseEvent& event)
{
    wxPoint pos = event.GetPosition();
    wxLogDebug("Mouse at: '(%d, %d)'", pos.x, pos.y);
}
And this is how it updates,
Image

It feels more like wxEVT_ENTER_WINDOW than wxEVT_MOTION. Or is this the normal behavior?
User avatar
doublemax
Moderator
Moderator
Posts: 19115
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to remove trailing effect when drawing on a panel?

Post by doublemax »

What else is your app doing at that time? Try to test this in a standalone sample.
Use the source, Luke!
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

doublemax wrote: Thu Jul 29, 2021 12:48 pm What else is your app doing at that time? Try to test this in a standalone sample.
Nothing else, I have a wxBitmap on the panel, the image (SVG) you can see in the GIF in the center. Could that be a issue?

EDIT: Yes, looks like this was the issue, I removed the wxBitmap, not it works fine, updating continuously.
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

doublemax wrote: Thu Jul 29, 2021 12:48 pm What else is your app doing at that time? Try to test this in a standalone sample.
Is is not possible to have 2 different paint events on the same panel? I also want to draw waveform on the same panel, and above the waveform, I want the seekbar.

Something like this,
Image

When I draw the waveform, the playhead drawing doesn't get painted.
User avatar
doublemax
Moderator
Moderator
Posts: 19115
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to remove trailing effect when drawing on a panel?

Post by doublemax »

Is is not possible to have 2 different paint events on the same panel? I also want to draw waveform on the same panel, and above the waveform, I want the seekbar.
I was just hinting at a potential performance issue. Is the panel constantly being redrawn, if yes at what rate? And how do you draw the SVG?
Use the source, Luke!
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

I was just hinting at a potential performance issue. Is the panel constantly being redrawn, if yes at what rate?
Yes, actually, after adding the drawing code for waveform (which is not perfect), the app is now very slow, almost not responding at times. I can't say about rate, but I have the wxTimer update 1ms, as anything above it was not smooth looking. Here is all the drawing code,

Code: Select all

void MainFrame::OnPaintPlayhead(wxPaintEvent& event)
{
    wxPaintDC dc(m_TopWaveformPanel);
    OnRenderPlayhead(dc);
    OnRenderWaveform(dc);
}

void MainFrame::OnRenderPlayhead(wxDC& dc)
{
    int selected_row = m_Library->GetSelectedRow();

    if (selected_row < 0)
        return;

    wxString selected = m_Library->GetTextValue(selected_row, 1);
    std::string path = GetFilenamePathAndExtension(selected).Path.ToStdString();

    Tags tags(path);

    int length = tags.GetAudioInfo().length;
    wxLogDebug("Len: %d", length);

    double position = m_MediaCtrl->Tell();
    wxLogDebug("Pos: %f", position);

    m_Timer->Start(1, wxTIMER_CONTINUOUS);

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

    wxLogDebug("Drawing at: %f", line_pos);

    // Draw the triangle
    dc.SetPen(wxPen(wxColor(255, 0, 0, 255), 8, wxPENSTYLE_SOLID));
    dc.DrawLine(line_pos - 5, m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight() - 1),
                line_pos, m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight() - 1) + 5);
    dc.DrawLine(line_pos + 5, m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight() - 1),
                line_pos, m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight()- 1) + 5);

    // Draw the line
    dc.SetPen(wxPen(wxColor(255, 0, 0, 255), 2, wxPENSTYLE_SOLID));
    dc.DrawLine(line_pos, m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight() - 1),
                line_pos, m_TopWaveformPanel->GetSize().GetHeight() - 1);
}

// void MainFrame::OnPaintWaveform(wxPaintEvent& event)
// {
//     wxPaintDC dc(m_TopWaveformPanel);
    // OnRenderWaveform(dc);
// }

void MainFrame::OnRenderWaveform(wxDC &dc)
{
    Database db(*m_InfoBar);
    Settings settings(this, m_ConfigFilepath, m_DatabaseFilepath);

    int selected_row = m_Library->GetSelectedRow();

    if (selected_row < 0) return;

    wxString selection = m_Library->GetTextValue(selected_row, 1);

    wxString filepath_with_extension = db.GetSamplePathByFilename(static_cast<std::string>(DATABASE_FILEPATH), selection.BeforeLast('.').ToStdString());
    wxString filepath_without_extension = db.GetSamplePathByFilename(static_cast<std::string>(DATABASE_FILEPATH), selection.ToStdString());

    std::string extension = settings.ShouldShowFileExtension() ?
        db.GetSampleFileExtension(static_cast<std::string>(DATABASE_FILEPATH), selection.ToStdString()) :
        db.GetSampleFileExtension(static_cast<std::string>(DATABASE_FILEPATH), selection.BeforeLast('.').ToStdString());

    wxString path = selection.Contains(wxString::Format(".%s", extension)) ?
        filepath_with_extension : filepath_without_extension;

    SndfileHandle snd_file(path);

    int channels = snd_file.channels();
    double sample_rate = snd_file.samplerate();
    sf_count_t frames = snd_file.frames();

    std::vector<float> sample;
    sample.resize(frames * channels);

    std::vector<signed char> waveform;

    snd_file.read(&sample.at(0), frames * channels);

    float display_width = m_TopWaveformPanel->GetSize().GetWidth();
    float display_height = m_TopWaveformPanel->GetSize().GetHeight();

    double max_value;
    snd_file.command(SFC_CALC_NORM_SIGNAL_MAX, &max_value, sizeof(max_value));

    float normalized_value = max_value > 1.0f ? 1.0f : 1.0f / max_value;

    float samples_per_pixel = static_cast<float>(frames) / (float)display_width;

    if (channels == 2)
    {
        for (int i = 0, j = 0 ; i < frames; i++)
        {
            float sum = (((sample[j] + sample[j + 1]) * 0.5f) * normalized_value) * float(display_height / 2.0f);
            waveform.push_back(sum);
            j += 2;
        }
    }
    else
    {
        waveform.resize(frames);

        for (int i = 0; i < frames; i++)
        {
            waveform[i] = (sample[i] * normalized_value) * float(display_height / 2.0f);
        }
    }

    /*===========================Draw code============================*/
    dc.SetPen(wxPen(wxColour(255, 255, 255, 255), 2, wxPENSTYLE_SOLID));

    for(int i = 0; i < waveform.size() - 1; i++)
    {
        // dc.DrawPoint((display_width * static_cast<float>(i)) / waveform.size(),
        //              (waveform[i] / samples_per_pixel) + float(display_height / 2.0f));

        // dc.DrawLine((display_width * static_cast<float>(i)) / waveform.size(),
        //             waveform[i] + float(display_height / 2.0f),
        //             (display_width * static_cast<float>(i)) / waveform.size(),
        //             waveform[i] + float(display_height / 2.0f));

        dc.DrawLine((display_width * i) / waveform.size(), waveform[i] + display_height / 2.0f,
                    (display_width * i) / waveform.size(), (waveform[i] / samples_per_pixel) + display_height / 2.0f);
    }
}
At first I had separate functions for drawing playhead and drawing waveform, but it was not drawing playhead when I added the code for waveform. So I decided to combine them. But the problem is the waveform code, doesn't need to update all the time, only when the splitter or the window is resized. Also I don't think drawing waveform realtime is a good idea, maybe I should draw a bitmap and use the bitmap to show waveform. Is it possible to save the drawing as a bitmap? PNG or SVG? SVG would be better, because scaling would be easier.

The waveform code is not mine, I borrowed it from a fellow developer. I took this code to understand how to draw waveform. I am still tweaking it.
And how do you draw the SVG?
I removed the SVG. But I was using wxStaticBitmap, and I passed the path to a SVG file.
User avatar
doublemax
Moderator
Moderator
Posts: 19115
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: How to remove trailing effect when drawing on a panel?

Post by doublemax »

That drawing code is far too slow. You need to (pre-)render it into a bitmap and only update the bitmap if the data or the visible area changes. In the paint event just draw the bitmap and the position indicator on top.
Use the source, Luke!
apoorv569
Super wx Problem Solver
Super wx Problem Solver
Posts: 426
Joined: Tue Oct 20, 2020 3:35 pm

Re: How to remove trailing effect when drawing on a panel?

Post by apoorv569 »

doublemax wrote: Thu Jul 29, 2021 4:47 pm That drawing code is far too slow. You need to (pre-)render it into a bitmap and only update the bitmap if the data or the visible area changes. In the paint event just draw the bitmap and the position indicator on top.
I see, how do I render the drawing to a bitmap? I tried with wxMemoryDC, which seems to work, but not how I intend it to. I made a member wxBitmap variable, and in the constructor, I have,

Code: Select all

    m_WaveformBitmap.LoadFile(WAVEFORM_SVG);
    m_WaveformViewer = new wxStaticBitmap(m_TopWaveformPanel, wxID_ANY, m_WaveformBitmap);
And the paint handler.

Code: Select all

void MainFrame::OnPaintPlayhead(wxPaintEvent& event)
{
    wxPaintDC dc(m_TopWaveformPanel);
    OnRenderPlayhead(dc);

    wxMemoryDC mdc;
    mdc.SelectObject(m_WaveformBitmap);

    if (!mdc.IsOk())
        return;

    OnRenderWaveform(mdc);

    dc.Blit(m_TopWaveformPanel->GetSize().GetWidth() - (m_TopWaveformPanel->GetSize().GetWidth() - 1),
            m_TopWaveformPanel->GetSize().GetHeight() - (m_TopWaveformPanel->GetSize().GetHeight() - 1),
            m_WaveformBitmap.GetWidth(), m_WaveformBitmap.GetHeight(), &mdc, 0, 0);

    m_WaveformViewer->SetBitmap(m_WaveformBitmap);
}
And it initially opens like this,
Image

And when I play a sample, it draws the waveform on top of the SVG image, and very small,
Image

Also it keeps updating the panel constantly, which makes the app very slow to respond.
Post Reply