Simple wxSwitchCtrl class

If you have a cool piece of software to share, but you are not hosting it officially yet, please dump it in here. If you have code snippets that are useful, please donate!
Post Reply
AmadeusK525
Experienced Solver
Experienced Solver
Posts: 60
Joined: Wed Aug 19, 2020 12:04 am

Simple wxSwitchCtrl class

Post by AmadeusK525 »

I've missed this feature a lot in wxWidgets and it's a simple switch, so I decided to make one. For now it only functions horizontally. It supports a label, which is positioned on top of it. There's a switch animation that slides the button and you can set its speed by calling wxSwitchCtrl::SetUnitsToTravel(). The higher the units, the longer the animation takes.

It sends a wxEVT_SWITCHING before it switches and a wxEVT_SWITCH after it switches, so you can stop the switch from happening. If there is a label, the control will take the width of the label, which might result in a wide switch. If this is undesired you'll have to make your own label instead of using the control's. I will try and fix this later.

You can change the color of the background, the button and the slider (both on and off states). There's also a global wxMixColours() functions, used when animating the switch.

SwitchCtrl.h:

Code: Select all

///////////////////////////////////////////////////////////////////////////////
// Name:        SiwtchCtrl.h
// Purpose:     Implementing a custom wxSwitchCtrl
// Author:      AmadeusK525
// Created:     2020-04-13
// Licence:     wxWindows licence
///////////////////////////////////////////////////////////////////////////////

#ifndef _WX_SWITCHCTRL_H_
#define _WX_SWITCHCTRL_H_
#pragma once

#include <wx/control.h>
#include <wx/stattext.h>
#include <wx/sizer.h>
#include <wx/timer.h>

wxColour WXDLLIMPEXP_CORE wxMixColours(const wxColour& firstColour, const wxColour& secondColour, int percent);

class WXDLLIMPEXP_CORE wxSwitchCtrl : public wxControl
{
public:
	wxSwitchCtrl() = default;
	wxSwitchCtrl(wxWindow* parent,
		wxWindowID id,
		bool value = false,
		const wxString& label = wxEmptyString,
		const wxPoint& pos = wxDefaultPosition,
		const wxSize& size = wxSize(30, 15),
		long style = wxBORDER_NONE,
		const wxValidator& validator = wxDefaultValidator,
		const wxString& name = wxControlNameStr);

	virtual void SetLabel(const wxString& label) wxOVERRIDE;
	virtual bool SetFont(const wxFont& font) wxOVERRIDE;
	virtual bool SetForegroundColour(const wxColour& colour) wxOVERRIDE;
	virtual wxSize DoGetBestClientSize() const wxOVERRIDE;

	inline void Switch() { DoSwitch(!m_bIsOn, true); }

	inline virtual void SetValue(bool state) { DoSwitch(state, false); }
	inline bool GetValue() { return m_bIsOn; }

	inline void SetEnabledColour(const wxColour& colour) { m_enabledColour = colour; }
	inline void SetDisabledColour(const  wxColour& colour) { m_disabledColour = colour; }

	/*!
	* \brief Set the number of units the buttons needs to travel when activaded.
	* The higher the number, the slower the animation on activate/deactivate
	* will be. Default is 10.
	* \param units Number of units
	*/
	inline void SetUnitsToScroll(int units) { m_nUnitCount = units; }

	wxDECLARE_EVENT_TABLE();

protected:
	void DoSwitch(bool state, bool sendEvent = true);
	void OnAnimationTimer(wxTimerEvent& event);

	void OnPaint(wxPaintEvent& event);
	void OnSize(wxSizeEvent& event);

	void OnLeftDown(wxMouseEvent& event);
	void OnLeftUp(wxMouseEvent& event);
	void OnMouseMove(wxMouseEvent& event);
	void OnMouseCaptureLost(wxMouseCaptureLostEvent& event);

	wxStaticText* m_labelST = nullptr;

	wxBoxSizer* m_sizer = nullptr;

	wxColour m_enabledColour{ 50, 50, 255 };
	wxColour m_disabledColour{ 100,100,100 };
	wxColour m_buttonColour{ 30,30,30 };

	bool m_bIsOn = false;

private:
	int m_nCurrentUnit = 0;
	int m_nCurrentButtonPos = 0;
	int m_yOrigin = 0;

	int m_nUnitCount = 10;
	double m_radius = 2.0;

	bool m_bWillDrag = false;
	bool m_bIsDragging = false;

	wxTimer m_tAnimationTimer;
	wxSize m_szCacheSize;
};

wxDECLARE_EVENT(wxEVT_SWITCH, wxCommandEvent);
wxDECLARE_EVENT(wxEVT_SWITCHING, wxCommandEvent);
#define EVT_SWITCH(winid, func) wx__DECLARE_EVT1(wxEVT_SWITCH, winid, wxCommandEventHandler(func))
#define EVT_SWITCHING(winid, func) wx__DECLARE_EVT1(wxEVT_SWITCHING, winid, wxCommandEventHandler(func))

#endif // _WX_SWITCHCTRL_H_
SwitchCtrl.cpp:

Code: Select all

#include "SwitchCtrl.h"
#include <wx/wx.h>
#include <wx/dcgraph.h>

wxColour wxMixColours(const wxColour& firstColour, const wxColour& secondColour, int percent)
{
	int newRed = (double)((secondColour.Red() * percent) + (firstColour.Red() * (100 - percent))) / 100;
	int newGreen = (double)((secondColour.Green() * percent) + (firstColour.Green() * (100 - percent))) / 100;
	int newBlue = (double)((secondColour.Blue() * percent) + (firstColour.Blue() * (100 - percent))) / 100;

	return wxColour((unsigned char)newRed, (unsigned char)newGreen, (unsigned char)newBlue);
}

// Implementing events
wxDEFINE_EVENT(wxEVT_SWITCH, wxCommandEvent);
wxDEFINE_EVENT(wxEVT_SWITCHING, wxCommandEvent);


wxBEGIN_EVENT_TABLE(wxSwitchCtrl, wxControl)

EVT_PAINT(wxSwitchCtrl::OnPaint)
EVT_SIZE(wxSwitchCtrl::OnSize)

EVT_LEFT_DOWN(wxSwitchCtrl::OnLeftDown)
EVT_LEFT_UP(wxSwitchCtrl::OnLeftUp)
EVT_MOTION(wxSwitchCtrl::OnMouseMove)
EVT_MOUSE_CAPTURE_LOST(wxSwitchCtrl::OnMouseCaptureLost)

EVT_TIMER(12345, wxSwitchCtrl::OnAnimationTimer)

wxEND_EVENT_TABLE()

wxSwitchCtrl::wxSwitchCtrl(wxWindow* parent,
	wxWindowID id,
	bool value,
	const wxString& label,
	const wxPoint& pos,
	const wxSize& size,
	long style,
	const wxValidator& validator,
	const wxString& name)
	: wxControl(parent, id, pos, size, style, validator, name), m_tAnimationTimer(this, 12345)
{
	m_sizer = new wxBoxSizer(wxVERTICAL);
	m_sizer->AddStretchSpacer(1);
	SetSizer(m_sizer);

	if ( !label.IsEmpty() )
		SetLabel(label);

	SetCursor(wxCURSOR_CLOSED_HAND);
	DoSwitch(value, false);
}

void wxSwitchCtrl::SetLabel(const wxString& label)
{
	if ( !m_labelST )
	{
		m_labelST = new wxStaticText(this, -1, label, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL);
		m_labelST->SetCursor(wxCURSOR_DEFAULT);
		m_labelST->SetFont(GetFont());
		m_labelST->SetForegroundColour(GetForegroundColour());

		m_sizer->Insert(0, m_labelST, wxSizerFlags(0).Expand().Border(wxBOTTOM, 1));
		m_sizer->AddSpacer(m_labelST->GetSize().y);
	}
	else
	{
		m_labelST->SetLabel(label);
	}

	wxControl::SetLabel(label);
	Layout();
}

bool wxSwitchCtrl::SetFont(const wxFont& font)
{
	if ( m_labelST )
		m_labelST->SetFont(font);

	wxControl::SetFont(font);
	return true;
}

bool wxSwitchCtrl::SetForegroundColour(const wxColour& colour)
{
	if ( m_labelST )
		m_labelST->SetForegroundColour(colour);

	wxControl::SetForegroundColour(colour);
	return true;
}

wxSize wxSwitchCtrl::DoGetBestClientSize() const
{
	if ( m_labelST )
	{
		wxSize labelSize = m_labelST->GetSize();
		return labelSize + wxSize(labelSize.x < 15 ? 20 : labelSize.x, labelSize.y);
	}
	else
	{
		return FromDIP(wxSize(m_radius * 4, m_radius * 2));
	}
}

void wxSwitchCtrl::DoSwitch(bool state, bool sendEvent)
{
	if ( state == m_bIsOn )
		sendEvent = false;

	if ( sendEvent )
	{
		wxCommandEvent event(wxEVT_SWITCHING, GetId());
		event.SetInt(state);
		event.Skip();
		ProcessEvent(event);

		if ( !event.GetSkipped() )
			return;
	}

	m_bIsOn = state;
	m_szCacheSize = GetClientSize();
	m_tAnimationTimer.Start(1);

	if ( sendEvent )
	{
		wxCommandEvent* event = new wxCommandEvent(wxEVT_SWITCH, GetId());
		event->SetInt(state);
		QueueEvent(event);
	}
}

void wxSwitchCtrl::OnAnimationTimer(wxTimerEvent& event)
{
	if ( m_bIsOn )
	{
		if ( m_nCurrentUnit < m_nUnitCount )
		{
			m_nCurrentUnit++;
		}
		else
		{
			m_nCurrentUnit = m_nUnitCount;
			m_tAnimationTimer.Stop();
		}
	}
	else
	{
		if ( m_nCurrentUnit > 0 )
		{
			m_nCurrentUnit--;
		}
		else
		{
			m_nCurrentUnit = 0;
			m_tAnimationTimer.Stop();
		}
	}

	m_nCurrentButtonPos = ((double)(m_nCurrentUnit * (m_szCacheSize.x - (m_radius * 2))) / m_nUnitCount);
	Refresh(true);
	Update();
}

void wxSwitchCtrl::OnPaint(wxPaintEvent& event)
{
	wxPaintDC dc(this);
	wxGCDC gdc(dc);
	wxSize clientSize = GetClientSize();

	wxColour slideColour = wxMixColours(m_disabledColour, m_enabledColour, (m_nCurrentUnit * 100) / m_nUnitCount);

	gdc.SetBrush(wxBrush(slideColour));
	gdc.SetPen(*wxTRANSPARENT_PEN);
	gdc.DrawRoundedRectangle(wxPoint(0, m_yOrigin), clientSize - wxSize(0, m_yOrigin), m_radius);

	gdc.SetBrush(wxBrush(m_buttonColour));
	gdc.SetPen(*wxTRANSPARENT_PEN);
	gdc.DrawCircle(wxPoint(m_radius + m_nCurrentButtonPos, clientSize.y - m_radius), m_radius - 1);
}

void wxSwitchCtrl::OnSize(wxSizeEvent& event)
{
	wxSize size = event.GetSize();

	if ( m_labelST )
	{
		wxSize labelSize = m_labelST->GetBestSize();
		size.y -= labelSize.y;
		m_yOrigin = labelSize.y;

		if ( size.y < labelSize.y )
			SetMinClientSize(wxSize(labelSize.x, labelSize.y * 2));
	}

	m_radius = (double)size.y / 2.0;
	m_nCurrentButtonPos = ((double)(m_nCurrentUnit * (size.x - (m_radius * 2))) / m_nUnitCount);

	event.Skip();
}

void wxSwitchCtrl::OnLeftDown(wxMouseEvent& event)
{
	// If the user presses clicks on the button, prepare to
	// drag, but don't change state to dragging yet.

	int buttonWidth = m_radius * 2;
	wxRect buttonRect(m_nCurrentButtonPos, GetClientSize().y - buttonWidth, buttonWidth, buttonWidth);

	if ( buttonRect.Contains(event.GetPosition()) )
		m_bWillDrag = true;
}

void wxSwitchCtrl::OnLeftUp(wxMouseEvent& event)
{
	if ( !m_bIsDragging )
		DoSwitch(!m_bIsOn);
	else
	{
		DoSwitch(m_nCurrentUnit >= m_nUnitCount / 2 ? true : false);
		ReleaseMouse();
	}

	m_bWillDrag = false;
	m_bIsDragging = false;
}

void wxSwitchCtrl::OnMouseMove(wxMouseEvent& event)
{
	if ( m_bWillDrag )
	{
		CaptureMouse();
		m_bIsDragging = true;
		m_bWillDrag = false;
	}

	if ( m_bIsDragging )
	{
		wxSize clientSize(GetClientSize());
		wxPoint mousePos(event.GetPosition());

		// Force input to be valid
		if ( mousePos.x < 0 )
			mousePos.x = 0;
		else if ( mousePos.x > clientSize.x )
			mousePos.x = clientSize.x;

		int unitWidth = clientSize.x / m_nUnitCount;

		// Calculate current button unit and its position
		m_nCurrentUnit = mousePos.x / unitWidth;
		m_nCurrentButtonPos = ((double)(m_nCurrentUnit * (clientSize.x - (m_radius * 2))) / m_nUnitCount);

		// Update the screen for the user
		Refresh();
		Update();
	}

	event.Skip();
}

void wxSwitchCtrl::OnMouseCaptureLost(wxMouseCaptureLostEvent& event)
{
	m_bWillDrag = false;
	m_bIsDragging = false;

	DoSwitch(m_nCurrentUnit >= m_nUnitCount / 2 ? true : false);
}
I don't know if the implementation is the most efficient, but it gets the job done AFAIK. I haven't encountered any bugs, but you can of course post any in this thread.
Oh and there are event macros for it: EVT_SWITCH(winid, func) and EVT_SWITCHING(winid, func)
wxSwitchCtrls.png
wxSwitchCtrls.png (1.73 KiB) Viewed 13453 times
Attachments
SwitchCtrl.h
(2.75 KiB) Downloaded 202 times
SwitchCtrl.cpp
(6.78 KiB) Downloaded 194 times
Last edited by AmadeusK525 on Fri May 14, 2021 5:25 pm, edited 4 times in total.
User avatar
doublemax
Moderator
Moderator
Posts: 19114
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Simple wxSwitchCtrl class

Post by doublemax »

Thanks for the contribution =D>

FWIW, as it's essentially a (dual state) wxCheckBox, i would have used the same event for it.
Use the source, Luke!
AmadeusK525
Experienced Solver
Experienced Solver
Posts: 60
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 »

Thank you for responding!
You're right, it is literally a checkbox but with eye candy. I decided to create custom events because wxCheckBox only sends the event of it being clicked and I wanted to send two of them, one when it's clicked and one when it switches, because they can be used independently. But I guess it could send the wxEVT_CHECKBOX, I'll think about changing it to that later
AmadeusK525
Experienced Solver
Experienced Solver
Posts: 60
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 »

(Edit 1):
Changed animation to be handled by a wxTimer (don't know why I hadn't thought about this before). Now it doesn't hog the program and it can take as long as you want without affecting the application's performance.
User avatar
doublemax@work
Super wx Problem Solver
Super wx Problem Solver
Posts: 474
Joined: Wed Jul 29, 2020 6:06 pm
Location: NRW, Germany

Re: Simple wxSwitchCtrl class

Post by doublemax@work »

Can you add the code as attachment to the post? That would make it easier for people to download, and you would see how often it gets downloaded.
AmadeusK525
Experienced Solver
Experienced Solver
Posts: 60
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 »

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

Re: Simple wxSwitchCtrl class

Post by doublemax »

AmadeusK525 wrote: Wed Apr 14, 2021 2:44 pmDone :)
Thanks!
Use the source, Luke!
ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 7458
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Simple wxSwitchCtrl class

Post by ONEEYEMAN »

Hi,
FWIW, GTK has this control natively, and MSW (WPF I think) has it as well.

GTK reference - https://docs.gtk.org/gtk4/class.Switch.html.

Not sure about the OSX thiough - it looks like it is available for OSX since 10.15 (ref. https://developer.apple.com/documentati ... guage=objc)/uiswitch?language=objc)

Maybe you can submit it as a PR to be included in the library itself.

Thank you.
AmadeusK525
Experienced Solver
Experienced Solver
Posts: 60
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 »

Hello ONEEYEMAN,
Thank you for the tip. Should I just open a PR as a generic control or do you mean I should look into implementing the control natively for each OS? I may try and make it a little more customizable and implement the drag functionality, then I'll see about submitting it to the library.
ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 7458
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Simple wxSwitchCtrl class

Post by ONEEYEMAN »

Hi,
IIUC this control can be implemented natively n GTK and OSX and MSW will use generic version.

If you are familiar with GTK/OSX and can create native implementation for those toolkit, it is better to make it as one PR for all 3 platforms.

But if you are not - submitting just you implementation with the links to the documentation should be fine.

You shouold also get yourself familiar with Bakefile as you willl need to modify their files and re-run Bakefile to re-generate Makefile's.

Also very important - add the documentation for the new class. Otherwise you contribution will be seriously delayed.

Try to read the documentation here

Thank you and good luck.
AmadeusK525
Experienced Solver
Experienced Solver
Posts: 60
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 »

(Edit 2)
- Reformatted the code to follow wxWidgets standard.
- Added user-slide functionality: the user is able to manually slide the button and change the state that way.
- Added 'this' to the event generated with wxEvent::SetEventObject().

Maybe I did some extra things, but as far as I remember those are the important changes :)
Kvaz1r
Super wx Problem Solver
Super wx Problem Solver
Posts: 357
Joined: Tue Jun 07, 2016 1:07 pm

Re: Simple wxSwitchCtrl class

Post by Kvaz1r »

Do you have a public repo with the project on any public host(Github/Gitlab/SourceHut/...)? It will be easy to track changes or propose them via PR.
Post Reply