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
Knows some wx things
Knows some wx things
Posts: 38
Joined: Wed Aug 19, 2020 12:04 am

Simple wxSwitchCtrl class

Post by AmadeusK525 » Tue Apr 13, 2021 2:38 pm

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 SWITCHCTRL_H_
#define SWITCHCTRL_H_
#pragma once

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

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

class wxSwitchCtrl : public wxControl {
private:
	int m_currentUnit = 0;
	int m_currentCenterPos = 0;
	int m_yOrigin = 0;

	int m_units = 10;
	double m_radius = 2.0;

	wxTimer m_animationTimer;
	wxSize m_cacheSize;

protected:
	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_isOn = false;

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) override;
	virtual bool SetFont(const wxFont& font) override;
	virtual bool SetForegroundColour(const wxColour& colour) override;
	virtual wxSize DoGetBestClientSize() const override;

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

	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_units = units; }

	DECLARE_EVENT_TABLE()

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

	void OnPaint(wxPaintEvent& event);
	void OnSize(wxSizeEvent& event);
	void OnLeftUp(wxMouseEvent& event);
};

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
SwitchCtrl.cpp:

Code: Select all

#include "SwitchCtrl.h"

#include <wx\dcbuffer.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);
}

wxDEFINE_EVENT(wxEVT_SWITCH, wxCommandEvent);
wxDEFINE_EVENT(wxEVT_SWITCHING, wxCommandEvent);

BEGIN_EVENT_TABLE(wxSwitchCtrl, wxControl)

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

EVT_LEFT_UP(wxSwitchCtrl::OnLeftUp)

EVT_TIMER(12345, wxSwitchCtrl::OnTimer)

END_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_animationTimer(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_isOn)
		return;

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

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

	m_isOn = state;
	m_cacheSize = GetClientSize();
	m_animationTimer.Start(1);

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

void wxSwitchCtrl::OnTimer(wxTimerEvent& event) {
	if (m_isOn)
		if (m_currentUnit < m_units) m_currentUnit++;
		else {
			m_currentUnit = m_units;
			m_animationTimer.Stop();
		}
	else
		if (m_currentUnit > 0) m_currentUnit--;
		else {
			m_currentUnit = 0;
			m_animationTimer.Stop();
		}

	m_currentCenterPos = ((double)(m_currentUnit * (m_cacheSize.x - (m_radius * 2))) / m_units);
	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_currentUnit * 100) / m_units);

	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_currentCenterPos, 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_currentCenterPos = ((double)(m_currentUnit * (size.x - (m_radius * 2))) / m_units);

	event.Skip();
}

void wxSwitchCtrl::OnLeftUp(wxMouseEvent& event) {
	DoSwitch(!m_isOn);
}
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 826 times
Attachments
SwitchCtrl.h
(2.41 KiB) Downloaded 16 times
SwitchCtrl.cpp
(4.84 KiB) Downloaded 15 times
Last edited by AmadeusK525 on Wed Apr 14, 2021 2:43 pm, edited 2 times in total.

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

Re: Simple wxSwitchCtrl class

Post by doublemax » Tue Apr 13, 2021 3:51 pm

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
Knows some wx things
Knows some wx things
Posts: 38
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 » Tue Apr 13, 2021 6:01 pm

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
Knows some wx things
Knows some wx things
Posts: 38
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 » Tue Apr 13, 2021 6:40 pm

(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
[email protected]
Experienced Solver
Experienced Solver
Posts: 87
Joined: Wed Jul 29, 2020 6:06 pm

Re: Simple wxSwitchCtrl class

Post by [email protected] » Wed Apr 14, 2021 7:37 am

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
Knows some wx things
Knows some wx things
Posts: 38
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 » Wed Apr 14, 2021 2:44 pm

Done :)

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

Re: Simple wxSwitchCtrl class

Post by doublemax » Wed Apr 14, 2021 3:58 pm

AmadeusK525 wrote:
Wed Apr 14, 2021 2:44 pm
Done :)
Thanks!
Use the source, Luke!

ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 5059
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Simple wxSwitchCtrl class

Post by ONEEYEMAN » Fri Apr 16, 2021 8:39 pm

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
Knows some wx things
Knows some wx things
Posts: 38
Joined: Wed Aug 19, 2020 12:04 am

Re: Simple wxSwitchCtrl class

Post by AmadeusK525 » Sun Apr 18, 2021 3:50 am

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: 5059
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: Simple wxSwitchCtrl class

Post by ONEEYEMAN » Sun Apr 18, 2021 5:37 am

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.

Post Reply