2
\$\begingroup\$

I wanted to make a bounded slider since the ranged slider wxwidgets includes isnt very useful for my use case. I wanted a left and right bounded slider to control a dynamic way to control a graph or plot x-range.

I got a good head start from a person who made a library in Python and ended up creating my own version in C++. I would appreciate any feedback on how I can improve my code or any errors I might have missed.

Using the widget requires wxWidgets 3.1.4; code can be found here https://github.com/georgeplusplus/wxWidgets-Custom-Ranged-Slider

It is just two files; RangeSlider.cpp and RangeSlider.h. This file has two classes - the RangeSlider itself, and SliderThumb, which is responsible for the two nubs. I would like some feedback on how I handled my mouse inputs and calculating positioning of the drawing of the widget elements and general structure. I think others could benefit from this widget since I havnt seen one in C++ so I would like to make sure its the best it can be to share.

Mainwindow

#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif // !WX_PRECOMP

#include "include/MainWindow.h"
#include "include/RangeSlider.h"

MainWindow::MainWindow(wxWindow* parent,
    wxWindowID id,
    const wxString& title,
    const wxPoint& pos,
    const wxSize& size,
    long style,
    const wxString& name) :
    wxFrame(parent, id, title, pos, size, style, name)
{
    SetSize(300, 100);

    wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
    SetSizer(mainSizer);
    
    RangeSlider* range_slider = new RangeSlider(this);

    mainSizer->Add(range_slider, 1,  wxEXPAND | wxALL, 6);
    
}

RangeSlider.h

#pragma once

#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif // !WX_PRECOMP

#include <wx/dcbuffer.h>

enum ThumbType
{
    LOW,
    HIGH,
};

class SliderThumb;

class RangeSlider : public wxPanel
{
public:

    explicit RangeSlider(wxWindow* parent,
                         wxWindowID id = wxID_ANY,
                         const wxPoint& pos = wxDefaultPosition,
                         const wxSize& size = wxDefaultSize,
                         long style = wxSL_HORIZONTAL,
                         const wxString& name = wxPanelNameStr);
    
    void PaintEvent(wxPaintEvent& event);
    void Render(wxDC& dc);

    int GetMax() { return max_value; };
    int GetMin() { return min_value; };

    void OnMouseDown(wxMouseEvent& event);
    void OnMouseUp(wxMouseEvent& event);
    void OnMouseLost(wxMouseCaptureLostEvent& event);
    void OnMouseMotion(wxMouseEvent& event);
    void OnMouseEnter(wxMouseEvent& event);
    void OnMouseLeave(wxMouseEvent& event);
    void OnResize(wxSizeEvent& event);

    const int border_width = 8;
    wxVector<SliderThumb> thumbs;

private:

    SliderThumb* selected_thumb = nullptr;

    int min_value = 0;
    int max_value = 10000;

    wxColor slider_background_color = wxColor(231, 234, 234);
    wxColor slider_outline          = wxColor(14, 14, 14);
    wxColor selected_range_color    = wxColor(0, 120, 215);
    wxColor selected_range_outline  = wxColor(0, 120, 215);
};

class SliderThumb
{
public:
    SliderThumb(RangeSlider& parent, int value, ThumbType type);
    
    bool IsMouseOver(wxPoint& click_pos);
    void Render(wxDC& dc);

    wxPoint GetPosition();
    void SetPosition(wxPoint mouse_position);
    const int GetThumbValue() const { return value_; };
    const ThumbType GetType() const { return type_; };
    const wxSize GetSize() const { return size_; };
    int GetMin();
    int GetMax();

    bool mouse_over = false;
    

private:

    RangeSlider* parent_;
    float value_ = 0.0;
    ThumbType type_;
    wxSize size_;
    

    wxPoint thumb_poly[5] = { wxPoint(0,0), wxPoint(0,13), wxPoint(5,18), wxPoint(10,13), wxPoint(10,0) };
    wxPoint thumb_shadow_poly[4] = { wxPoint(0,14), wxPoint(4,18), wxPoint(6,18), wxPoint(10,14) };

    
    wxColor normal_color        = wxColor(0, 120, 215);
    wxColor normal_shadow       = wxColor(120, 180, 228);
    wxColor dragged_color       = wxColor(204, 204, 204);
    wxColor dragged_shadow      = wxColor(222, 222, 222);
    wxColor mouse_over_color    = wxColor(100, 100, 100);
    wxColor mouse_over_shadow   = wxColor(132, 132, 132);

};

RangeSlider.cpp

#include "RangeSlider.h"

#include <wx/dcbuffer.h>

#include <algorithm>


float fraction_to_value(float fraction, int min_value, int max_value)
{
    return (max_value - min_value) * fraction + min_value;
}

float value_to_fraction(float value, int min_value, int max_value)
{
    return float(value - min_value) / (max_value - min_value);
}


RangeSlider::RangeSlider(wxWindow* parent, wxWindowID id,const wxPoint& pos ,const wxSize& size ,long style ,const wxString& name) 
    : wxPanel(parent, id, pos,size, style, name)
{

    SetMinSize(wxSize(std::max({50, size.GetX()}), std::max({26, size.GetY()})));

    SliderThumb low_thumb(*this, min_value, ThumbType::LOW);
    SliderThumb high_thumb(*this, max_value, ThumbType::HIGH);

    thumbs.push_back(low_thumb);
    thumbs.push_back(high_thumb);

    Bind(wxEVT_LEFT_DOWN, &RangeSlider::OnMouseDown, this, wxID_ANY);
    Bind(wxEVT_LEFT_UP, &RangeSlider::OnMouseUp, this, wxID_ANY);
    Bind(wxEVT_MOTION, &RangeSlider::OnMouseMotion, this, wxID_ANY);
    Bind(wxEVT_MOUSE_CAPTURE_LOST, &RangeSlider::OnMouseLost, this, wxID_ANY);
    Bind(wxEVT_ENTER_WINDOW, &RangeSlider::OnMouseEnter, this, wxID_ANY);
    Bind(wxEVT_LEAVE_WINDOW, &RangeSlider::OnMouseLeave, this, wxID_ANY);
    Bind(wxEVT_PAINT, &RangeSlider::PaintEvent, this, wxID_ANY);
    Bind(wxEVT_SIZE, &RangeSlider::OnResize, this, wxID_ANY);
}

void RangeSlider::PaintEvent(wxPaintEvent& event)
{
    // depending on your system you may need to look at double-buffered dcs
    wxBufferedPaintDC dc(this);
    Render(dc);

    event.Skip();
}

void RangeSlider::Render(wxDC& dc)
{
    
    auto background_brush = wxBrush(GetBackgroundColour(), wxBRUSHSTYLE_SOLID);
    dc.SetBackground(background_brush);
    dc.Clear();
    
    int track_height = 12;
    auto w = GetSize().GetX();
    auto h = GetSize().GetY();
    
    dc.SetPen(wxPen(slider_outline, 1, wxPENSTYLE_SOLID));
    dc.SetBrush(wxBrush(slider_background_color, wxBRUSHSTYLE_SOLID));
    dc.DrawRectangle(border_width, (h - track_height) / 2, w - 2 * border_width, track_height);
    
    if (IsEnabled())
    {
        dc.SetPen(wxPen(selected_range_outline, 1, wxPENSTYLE_SOLID));
        dc.SetBrush(wxBrush(selected_range_color, wxBRUSHSTYLE_SOLID));
    }
    else
    {
        dc.SetPen(wxPen(slider_outline, 1, wxPENSTYLE_SOLID));
        dc.SetBrush(wxBrush(slider_outline, wxBRUSHSTYLE_SOLID));
    }

    auto low_thumb_pos = thumbs[0].GetPosition().x;

    auto high_thumb_pos = thumbs[1].GetPosition().x;
    dc.DrawRectangle(low_thumb_pos, h / 2 - track_height / 4, high_thumb_pos - low_thumb_pos, track_height / 2);

    for (auto& thumb : thumbs)
    {
        thumb.Render(dc);
    }

}

void RangeSlider::OnMouseDown(wxMouseEvent& event)
{
    
    if (!IsEnabled())
    {
        return;
    }

    auto click_pos = event.GetPosition();
    for (auto& thumb : thumbs)
    {
        if (thumb.IsMouseOver(click_pos))
        {
            selected_thumb = &thumb;
            break;
        }
    }

    CaptureMouse();
    Refresh();

}

void RangeSlider::OnMouseUp(wxMouseEvent& event)
{
    
    if (!IsEnabled())
    {
        return;
    }
    selected_thumb = nullptr;

    if (HasCapture())
    {
        ReleaseMouse();
    }
}

void RangeSlider::OnMouseLost(wxMouseCaptureLostEvent& event)
{
    event.Skip();
}

void RangeSlider::OnMouseMotion(wxMouseEvent& event)
{
    
    if (!IsEnabled())
    {
        return;
    }

    bool refresh_needed = false;
    wxPoint mouse_position = event.GetPosition();

    if(selected_thumb)
    {

        if (selected_thumb->GetType() == ThumbType::LOW)
        {
            if(mouse_position.x + selected_thumb->GetSize().x > thumbs[1].GetPosition().x)
            {
                // Then Mouse position equals the last known good thumb pos.
                mouse_position = selected_thumb->GetPosition();
            }
        }
        else
        {
            if (mouse_position.x - selected_thumb->GetSize().x < thumbs[0].GetPosition().x)
            {
                // Then Mouse position equals the last known good thumb pos.
                mouse_position = selected_thumb->GetPosition();
            }
        }
        
        selected_thumb->SetPosition(mouse_position);
        refresh_needed = true;
    }
    else
    {
        
        for (auto& thumb : thumbs)
        {
            bool old_mouse_over = thumb.mouse_over;
            thumb.mouse_over = thumb.IsMouseOver(mouse_position);
            if (old_mouse_over != thumb.mouse_over)
            {
                refresh_needed = true;
            }
        }
    }
    
    if (refresh_needed)
    {
        Refresh();
    }

}

void RangeSlider::OnMouseEnter(wxMouseEvent& event)
{
    event.Skip();
}

void RangeSlider::OnMouseLeave(wxMouseEvent& event)
{
    if (!IsEnabled())
    {
        return;
    }

    for (auto& thumb : thumbs)
    {
        thumb.mouse_over = false;
    }
    
    Refresh();
    
}

void RangeSlider::OnResize(wxSizeEvent& event)
{
    Refresh();
}



SliderThumb::SliderThumb(RangeSlider& parent, int value, ThumbType type)
{
    parent_ = &parent;
    value_ = value;
    type_ = type;
    size_ = wxSize(10, 18);
}

void SliderThumb::Render(wxDC& dc)
{
    wxColor thumb_color;
    wxColor thumb_shadow_color;
    if (!parent_->IsEnabled())
    {
        thumb_color = dragged_color;
        thumb_shadow_color = dragged_shadow;
    }
    else if (mouse_over)
    {
        thumb_color = mouse_over_color;
        thumb_shadow_color = mouse_over_shadow;
    }
    else
    {
        thumb_color = normal_color;
        thumb_shadow_color = normal_shadow;
    }

    auto thumb_pos = GetPosition();

    dc.SetBrush(wxBrush(thumb_shadow_color, wxBRUSHSTYLE_SOLID));
    dc.SetPen(wxPen(thumb_shadow_color, 1,  wxPENSTYLE_SOLID));
    dc.DrawPolygon(WXSIZEOF(thumb_shadow_poly), thumb_shadow_poly, thumb_pos.x - size_.x / 2, thumb_pos.y - size_.y / 2);
    
    dc.SetBrush(wxBrush(thumb_color, wxBRUSHSTYLE_SOLID));
    dc.SetPen(wxPen(thumb_color, 1, wxPENSTYLE_SOLID));
    dc.DrawPolygon(WXSIZEOF(thumb_poly), thumb_poly, thumb_pos.x - size_.x / 2, thumb_pos.y - size_.y / 2);
}

wxPoint SliderThumb::GetPosition()
{
    auto min_x = GetMin();
    auto max_x = GetMax();
    auto parent_size = parent_->GetSize();
    auto min_value = parent_->GetMin();
    auto max_value = parent_->GetMax();
    auto fraction = value_to_fraction(value_, min_value, max_value);
    wxPoint pos(fraction_to_value(fraction, min_x, max_x), parent_size.GetY() / 2 + 1);

    return pos;
}

bool SliderThumb::IsMouseOver(wxPoint& click_pos)
{
    auto pos = GetPosition();

    auto boundary_low = pos.x - size_.x / 2;
    auto boundary_high = pos.x + size_.x / 2;
    mouse_over =  (click_pos.x >= boundary_low && click_pos.x <= boundary_high);

    return mouse_over;

}
void SliderThumb::SetPosition(wxPoint mouse_position)
{
    auto pos_x = mouse_position.x;

    pos_x = std::min(std::max(pos_x, GetMin()), GetMax());
    auto fraction = value_to_fraction(pos_x, GetMin(), GetMax());
    value_ = fraction_to_value(fraction, parent_->GetMin(), parent_->GetMax());
    
}

int SliderThumb::GetMin()
{
    return parent_->border_width + size_.x / 2;
}

int SliderThumb::GetMax()
{
    auto parent_size = parent_->GetSize();
    return (parent_size.GetX() - parent_->border_width - size_.x / 2);
}

enter image description here

\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

Move class SliderThumb into class RangeSlider

A SliderThumb is just an implementation detail of a RangeSlider, so you can nest it inside class RangeSlider. You can then also rename it to just Thumb, to avoid having to repeat the Slider. Here is what it looks like:

class RangeSlider : public wxPanel {
    class Thumb {
        ...
    };

public:
    ...
    wxVector<Thumb> thumbs;
    ...
};

Make more member variables private

Avoid making member variables public if they should not be part of the public API. Also consider whether you want raw variables to be exposed, or whether it is better to add getter/setter functions for them. I would make border_width a private variable, and add a GetBorderWidth() function.

The vector thumbs should definitely be made private, nothing should be able to modify that vector except SliderThumb's own member functions.

Make ThumbType an enum class inside class Thumb

Make ThumbType an enum class for some extra type safety. Also, since the type of a thumb is just an implementation detail of a thumb, move this inside class Thumb:

class RangeSlider : public wxPanel {
    class Thumb {
    public:
        enum class Type {
            LOW,
            HIGH,
        };

        Thumb(RangeSlider& parent, int value, Type type);
        ...
    };
    ...
};

Store the two thumbs in a std::array

You always have exactly two thumbs, so it doesn't make sense to store them in a wxVector. Just use std::array for this. You also don't have to push items to the array in the constructor, you can just initialize it where you declare the array:

class RangeSlide : public wxPanel {
    ...
    std::array<Thumb, 2> thumbs = {{
        {*this, min_value, Thumb::Type::LOW},
        {*this, max_value, Thumb::Type::HIGH},
    }};
    ...
};

The double braces are necessary.

Make constants static constexpr where possible

There are several constants or variables that are effectively constant in your code, like border_width, min_value, max_value, and the colors. If they are really meant to be constant, make them static constexpr.

Fix quick motions making the slider look stucked

If I grab the right thumb and quickly move the mouse left past the left thumb, the right thumb hasn't moved all the way left, but is stuck somehwere halfway. You should fix the logic in OnMouseMotion() to ensure you always update mouse_position, but limit it such that you cannot make the two thumbs cross each other:

switch (selected_thumb->GetType()) {
case Thumb::Type::LOW:    
    mouse_position.x = std::min(mouse_position.x, thumbs[1].GetPosition().x);
    break;
case Thumb::Type::HIGH:    
    mouse_position.x = std::max(mouse_position.x, thumbs[0].GetPosition().x);
    break;
}

selected_thumb->SetPosition(mouse_position);

The above allows both thumbs to overlap each other completely, but you can easily modify it to prevent that by adding/subtracting the size from thumbs[?].GetPosition().x.

Use std::clamp() if possible

I see you want to limit some values to be between a minimum and maximum value, and you are nesting std::min() and std::max(). Since C++17, you can use std::clamp() to do that in an easier way.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.