laitimes

League of Legends style slider with WPF |

author:opendotnet

Widget name: RiotSlider

作者:Vicky&James

Source link: https://github.com/vickyqu115/riotslider tutorial video (https://www.bilibili.com/video/BV1uy421a7yM)

This article is a technical review of the WPF RiotSlider tutorial videos, and you can search our account to watch the full video tutorial content.
League of Legends style slider with WPF |
Detailed mechanics for analyzing and customizing WPF Slider controls

In WPF, basic controls like Button and ToggleButton have a simple structure and logic and can be implemented entirely in XAML without the need for background code. In contrast, more complex controls such as TextBox, ComboBox, and Slider require complex C# code with XAML to do what it does.

Understanding and applying the complex configuration of WPF controls allows for more elegant and flexible design and development of custom controls. By mastering these basic components, we can solve some of the problems in the MVVM development model and build higher-quality WPF applications.

This exploration of the WPF Slider widget is all about gaining a deeper understanding of how it's built and its inner workings. Despite the sheer volume of code, it's nearly impossible to get inside every WPF control, but there's no need to worry too much.

All of WPF's source code is open source and managed on GitHub. This means that we can always find and analyze specific controls as needed.

In addition to the Slider controls, we plan to dissect and analyze more complex and diverse controls in the future. So if you're interested in this part of the story, don't forget to follow our Bilibili or Youtube channels, and we'll share the source code on GitHub.

League of Legends style slider with WPF |

Content catalog

  1. WPF series of tutorials
  2. specification
  3. Create an app project
  4. Analyze the main features of Slider
  5. Extract the original style process
  6. Extract the source code for analysis
  7. Inspect the backend code (GitHub open source)
  8. Cross-platform OnApplyTemplate
  9. To summarize the Slider analysis
  10. Create a Riot-style Slider (custom controls)
  11. Project creation and start preparation
  12. TextBlock(Hi Slider)
  13. Add references and test executions
  14. Set the size of the Riot Slider
  15. PART_Track
  16. Add a slider
  17. Adjust the gap between the slider and the track
  18. PART_SelectionRange
  19. Add Riot-inspired design elements
  20. Implement a Riot-style slider
  21. Declare a slider resource
  22. Completing the RiotSlider Template (Finishing touches)
  23. Final Comments

WPF series of tutorials

So far, we've published four tutorial series on YouTube and BiliBili. English and Chinese dubbing is also available, with Korean subtitles. We hope that these videos, combined with detailed source code and detailed explanations, will improve your understanding of WPF.
  • [x] 主题切换:BiliBili,GitHub
  • [x] Riot ??:BiliBili,GitHub
  • [x] 魔法导航栏:BiliBili,GitHub
  • [x] Riot ??:BiliBili,GitHub

specification

The project is based on .NET Core, but is limited to the Windows platform due to the use of WPF. The project can be run with VS2022 with .NET 8.0 or on JetBrains' Rider.

  • [x] 操作系统:Microsoft Windows 11
  • [x] IDE:Microsoft Visual Studio 2022
  • [x] 版本:C# / .NET 8.0 / WPF / 仅限 Windows
  • [x] NuGet:Jamesnet.Wpf

It is recommended to use the latest version of Windows as the operating system. Of course, if you consider extending the platform to Avalonia UI, Uno Platform, MAUI, etc., you can also use MacOS as a secondary device. We also use both Thinkpad and MacBooks in our programming environment. It's important to note that Visual Studio isn't available on MacOS or Linux-based systems, so Rider is the only alternative. vscode

3. Create an app project

First, we need to create a WPF Application project.

  • [x] 项目类型:WPF Application
  • [x] Project Name: DemoApp
  • [x] Project version: .NET 8.0

4. Analysis of the main functions of Slider

Unlike simple controls like Buttons, WPF Slider controls have a very large number of properties. In particular, these properties play an important role in the functionality of the control, so it is worth taking a closer look. Some of these special main attributes are as follows:

Orientation:

The controls provided by WPF are generally generic in nature. Slider controls are no exception, with the Orientation property being an example. With this property, you can specify a horizontal or vertical orientation.

Orientation 属性也可以在 StackPanel 控件中找到。 StackPanel 的 Orientation 属性默认值为 Vertical,但 Slider 的 Orientation 属性默认值为 Horizontal。 因此,通常情况下,Slider 是以 Horizontal 形式出现,可能很多人不知道还有 Orientation 功能。

To help you better understand the Orientation property, let's look at a simplified Slider example.

<Style TargetType="{x:Type Slider}">
<Setter Property="Template" Value="{StaticResource SliderHorizontal}"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Template" Value="{StaticResource SliderVertical}"/>
</Trigger>
</Style.Triggers>
</Style>
           

Here we can see that the trigger switches (ControlTemplate) templates based on the Orientation property. Therefore, by looking at the detailed structure of this control, it is easy to understand the importance of the Orientation property.

That's an interesting part. Can you imagine or apply the concept of switching templates via Orientation before seeing the original code? Open source projects can really inspire developers in this way. Also, with this code, we can confirm that the best time to switch templates is in "Style.Trigger".

In this tutorial video, we'll only implement the Horizontal orientation, so we won't be branching via Orientation. However, it is also recommended that you try to implement the Vertical direction and submit a Pull Request to us through a fork, which is a small task.

So let's take a look at the effects of applying the Horizontal and Vertical properties respectively:

  • [x] Orientation: Horizontal
League of Legends style slider with WPF |
This can also be seen in the SelectionRange (blue) area to be discussed below.
  • [x] Orientation: Vertical
League of Legends style slider with WPF |
类似于这种切换(ControlTemplate)模板的控件还有很多,比如 ScrollViewer 等。
Minimum, Maximum 和 Value:

These properties represent the minimum/maximum value and the current value, respectively, and they are both properties of type double. Internally, the size and scale of the control are automatically calculated based on these values, as well as the position of the Range and Value values.

Since these properties are all DependencyProperties, they can be dynamically interacted with through bindings. For example, in an MVVM structure, you can use these three values to dynamically change the scope based on a specific scenario or to implement a variety of interesting applications.

SelectionStart, SelectionEnd 和 IsSelectionRangeEnabled:

These two properties (SelectionStart/SelectionEnd) are used to set a specific area. Actually, this area doesn't contain any special features, just to specify a compartment and highlight it visually. IsSelectionRangeEnabled is a property used to indicate whether the region is enabled or not, and depending on whether it is enabled, the value of the Visibility property (Visible/Collapsed) of the region is toggled by a trigger.

To sum up, these features are only for simple area display, so it can be confusing whether or not they are needed. But since they are common in different designs and fields, the need for it can be understood and foreseen. Respect the style preferences of 20 years ago

In fact, when these are applied in combination with Value values, they can have very interesting effects, as shown below:

<Slider Orientation="Horizontal"
Minimum="0"
Maximum="100"
Value="30"
SelectionStart="0"
SelectionEnd="{Binding Value, RelativeSource={RelativeSource Self}"
IsSelectionRangeEnabled="True"/>
           

Surprisingly, the Value value is bound via SelectionEnd, and the Selection (Range) range changes dynamically each time the value changes. I wonder if the WPF development team did it on purpose? All in all is great, and the implementation is very simple.

This part plays a very important role in the Riot-style Slider (CustomControl) that will be implemented later, so please pay attention.

5. The process of extracting the original style

As shown above, WPF is managed in an open-source manner through GitHub repositories, so we can see the source code for all controls. But since the repository contains the solution and all the projects and files, it is very difficult to extract only the contents of a specific control section.

Luckily, Visual Studio provides the ability to extract the default style (Template) for a specific control and render it as a GUI. Therefore, we can easily extract the corresponding code without having to look for it step by step in the open source code.

Similar to Identity Scaffolding in Blazor (although slightly different in nature, it can be compared to this for the sake of understanding).

On top of that, when the original styles are extracted through Visual Studio, they are actually connected as modifiable assets, so you can customize the design and functionality right away. Therefore, not only Slider, but also the original styles and templates of all controls can be extracted, which has a high application value in the process of WPF's research and learning.

This extraction is not necessarily available in commercial components such as Infragistics, Syncfusion, ArticPro, etc. Each company has a different scope and policy of disclosure, and in most cases, it is more likely to modularize and lead customization through DataTemplates rather than ControlTemplates. So, if you're interested, you can also look into the way of using the components.
Extraction Methods and Steps: Visual Studio
  • [x] 提取默认控件(Slider)样式(Edit a Copy... )
  • [x] 提取到当前文件(This document)
  • [x] 提取到 App.xaml 文件(Application)
  • [x] 创建新的 ResourceDictionary 文件并提取(Resource Dictionary)

However, the extraction step can only be done in the design area of the UserControl interface in the Partial form, where you can select the control and right-click on it. In this process, you need to select the Specify Style Name/Specify Copy Location for Extracted Styles option.

Can you try to look it up in VScode or Rider as well to see if there is a similar feature available?

Let's take a look at the process step by step.

  • [x] 样式提取命令:Slider > 右键点击 > Edit Template > Edit a Copy...
League of Legends style slider with WPF |
If extractable styles are not provided, then this option will not be activated.
  • [x] 样式提取选项窗口:Create ControlTemplate Resource(Window)
League of Legends style slider with WPF |
选择 Name(Key)和 Define in 选项,

In general, specifying a name is the right choice for testing or administration. If you select the "Apply to all" item without specifying a name, the resulting style will be applied globally based on the defined extraction location. So you need to fully understand this and extract it carefully.

In the video, you set the name and specify the definition location as Application. As a result, the extracted resources (if the file exists) will be included in the Resources area of the App.xaml file.

Personally, it is recommended that when doing this extraction, it should be carried out in a new project as much as possible in a test nature. Actually doing this in a live project can lead to some minor bugs and problems, so it's a good option from the standpoint of avoiding side effects.

6. Extracted source code analysis

As shown in the tutorial video, we have successfully extracted the style of the Slider control. Let's take a look at the resources in the App.xaml file and examine the important considerations one by one.

确认 Orientation 分支:

In explaining the Orientation property earlier, we briefly mentioned the triggers and switching mechanism, but now let's actually take a look at the source code of the implementation.

The following styles are extracted WPF base style originals that contain the SliderStyle1 template. (The actual application is error-free and works properly.) )

<Style x:Key="SliderStyle1" TargetType="{x:Type Slider}">
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource SliderThumb.Static.Foreground}"/>
<Setter Property="Template" Value="{StaticResource SliderHorizontal}"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Template" Value="{StaticResource SliderVertical}"/>
</Trigger>
</Style.Triggers>
</Style>
           

从内容来看,默认的 Template 被设置为 SliderHorizontal(ControlTemplate),通过触发器,当 Orientation 属性值为 Vertical 时,Template 切换为 SliderVertical(ControlTemplate)。

In this way, the structure of the style can be seen at a glance by modularly managing the ControlTemplate template. Even in situations where switching isn't required, it's a management style worth trying. We use this approach a lot, and we get inspiration from the process.

As a result, the actual functionality of the Slider control is implemented in the SliderHorizontal and SliderVertical ControlTemplate templates, respectively.

Now let's take a look at the default SliderHorizontal(ControlTemplate) template.

确认ControlTemplate:

Check out the Horizontal/Vertical dedicated templates separately. It can be found in the App.xaml file.

  • [x] Check the Horizontal-specific template
  • [x] 检查 Vertical 专用模板

ControlTemplate: SliderHorizontal

<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
<Border ...>
 ...
</Border>
<ControlTemplate.Triggers>
 ...
</ControlTemplate.Triggers>
</ControlTemplate>
           

ControlTemplate: SliderVertical

<ControlTemplate x:Key="SliderVertical" TargetType="{x:Type Slider}">
<Border ...>
 ...
</Border>
<ControlTemplate.Triggers>
 ...
</ControlTemplate.Triggers>
</ControlTemplate>
           

As shown above, the source code for Horizontal/Vertical is implemented separately. The implementation of both is the same, differing only in the direction of the design.

Name:

PART_

In the structure of a custom control, it is important to keep the XAML and the code backend tightly connected. However, in order for this connection to take place, it is necessary to pass through

GetTemplateChild

method to find the control name, which is not ideal in terms of readability. In order to improve this development method and systematically manage it, it is used

PART_

Naming conventions.

It's a naming convention, and it's passed at all

GetTemplateChild

Find the name of the control with the prefix in front of it

PART_

prefix to speculate on its functionality in XAML. Therefore, when analyzing a control template, if you find a control template

PART_

The names of the controls at the beginning, you can guess that they are required elements, and anticipate in advance the possible side effects of removing them.

Ultimately, this is very helpful for implementing custom controls. In addition, this rule is not only applicable in WPF, but is also common in other cross-platform frameworks that share XAML, and is an important and important part that cannot be ignored.

Slider

controls

PART_

control

  • PART_Track

  • PART_SelectionRange

The results showed that in addition to these two

PART_

Outside of controls, no other controls are used in the code backend. This is guaranteed by this naming convention. Therefore, it is very important to strictly follow this rule in the development of custom controls.

Test: Intentional change

PART_Track

name after checking the impact

Now, we intentionally change it

PART_Track

The name of the control.

<Track x:Name="PART_Track1" Grid.Row="1">
 ...
</Track>
           
Please double-check if it is there

SliderHorizontal

area.

When running the application, as in the tutorial video, drag it no matter what,

Track

target

Thumb

will not move left and right.

Thumb

The reason why it can't be moved is because the code backend can't get through because the name was changed intentionally before

GetTemplateChild

way to find

PART_Track

Control.

Therefore, since it cannot be found

PART_Track

control, there is no target when dragging the mouse

Thumb

to move. will be named

PART_Track1

Revert to the original

PART_Track

, the functionality will return to normal.

This phenomenon can also be found in many other basic controls, such as TextBox

PART_ContentHost

is one of them.

Test: Intentional change

PART_SelectionRange

name after checking the impact

Next, we also intentionally change

PART_SelectionRange

The name of the control.

<Rectangle x:Name="PART_SelectionRange1" .../>
           
Please double-check if it is there

SliderHorizontal

area. (x2)

Next, we need to change the trigger section used

PART_SelectionRange

section.

<Trigger Property="IsSelectionRangeEnabled" Value="true">
<Setter Property="Visibility" TargetName="PART_SelectionRange1" Value="Visible"/>
</Trigger>
           
Please double-check if it is there

SliderHorizontal

area. (x3)

at

Slider

we also need to set the following properties to enable it

PART_SelectionRange

<Slider Style="{DynamicResource SliderStyle1}"
Minimum="0" Maximum="100"
SelectionStart="0" SelectionEnd="50"
IsSelectionRangeEnabled="True"/>
           
Setup required

Minimum

Maximum

and

SelectionStart

SelectionEnd

IsSelectionRange

and so on to enable the range area.

Before:

PART_SelectionRange

League of Legends style slider with WPF |
Before the change, you can see the range area that is displayed normally.

After the change:

PART_SelectionRange1

League of Legends style slider with WPF |
After the change, the range area is no longer displayed.

Again, due to the inability to find it inside

PART_SelectionRange

control, the target of the range area cannot be calculated.

It can be seen that WPF controls, although relatively loosely constructed, have a modular structure. Therefore, if we can take advantage of these features, we can not only make good use of the features that have already been implemented, but also eliminate a lot of unnecessary features.

7. Code behind 确认 (GitHub 开源代码)

We discussed this in detail earlier

PART_

Naming conventions for controls and their impact, it's time to look at how these controls are used in actual classes.

The Code behind region cannot be confirmed by direct extraction. Therefore, we need to look through the official source code in the WPF codebase. In this section, we recommend checking out our video tutorial to learn how to view it.

In the actual source code, each

PART_

The names of the controls are defined as follows

string

private const string TrackName = "PART_Track";
private const string SelectionRangeElementName = "PART_SelectionRange";
           
Because the name is fixed, this naming convention must be observed.
WPF: OnApplyTemplate

Let's start with this section of the Track and SlectionRange from the (ControlTemplate) template.

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

 SelectionRangeElement = GetTemplateChild(SelectionRangeElementName) as FrameworkElement;
 Track = GetTemplateChild(TrackName) as Track;

if (_autoToolTip != )
 {
 _autoToolTip.PlacementTarget = Track !=  ? Track.Thumb : ;
 }
}
           
Note: The (Override) OnApplyTemplate method is called after the class and style are associated, so this is the best time to use GetTemplateChild.

Looking at the source code, we can see that they are defined as FrameworkElement and Track, respectively.

  • [x] PART_SelectionRange: SelectionRangeElement (FrameworkElement)
  • [x] PART_Track: TrackName (Track)

It's important to note here that the type of the Track is the same as in XAML, but the SelectionRange is defined as a FrameworkElement, not the original Rectangle, which means that the SelectionRange can be any type of control, not just a Rectangle. This is a deliberate attempt to make the type definition more flexible.

Therefore, we can speculate that the SelectionRangeElement (defined as the FrameworkElement type) only handles the basic functionality that this type can handle.

下面是实际处理 SelectionRangeElement 的部分。

private void UpdateSelectionRangeElementPositionAndSize()
{
 Size trackSize = new Size(0d, 0d);
 Size thumbSize = new Size(0d, 0d);

if (Track ==  || DoubleUtil.LessThan(SelectionEnd,SelectionStart))
 {
return;
 }

 trackSize = Track.RenderSize;
 thumbSize = (Track.Thumb != ) ? Track.Thumb.RenderSize : new Size(0d, 0d);

double range = Maximum - Minimum;
double valueToSize;

 FrameworkElement rangeElement = this.SelectionRangeElement as FrameworkElement;

if (rangeElement == )
 {
return;
 }

if (Orientation == Orientation.Horizontal)
 {
// Calculate part size for HorizontalSlider
if (DoubleUtil.AreClose(range, 0d) || (DoubleUtil.AreClose(trackSize.Width, thumbSize.Width)))
 {
 valueToSize = 0d;
 }
else
 {
 valueToSize = Math.Max(0.0, (trackSize.Width - thumbSize.Width) / range);
 }

 rangeElement.Width = ((SelectionEnd - SelectionStart) * valueToSize);
if (IsDirectionReversed)
 {
 Canvas.SetLeft(rangeElement, (thumbSize.Width * 0.5) + Math.Max(Maximum - SelectionEnd, 0) * valueToSize);
 }
else
 {
 Canvas.SetLeft(rangeElement, (thumbSize.Width * 0.5) + Math.Max(SelectionStart - Minimum, 0) * valueToSize);
 }
 }
else
 {
// Calculate part size for VerticalSlider
if (DoubleUtil.AreClose(range, 0d) || (DoubleUtil.AreClose(trackSize.Height, thumbSize.Height)))
 {
 valueToSize = 0d;
 }
else
 {
 valueToSize = Math.Max(0.0, (trackSize.Height - thumbSize.Height) / range);
 }

 rangeElement.Height = ((SelectionEnd - SelectionStart) * valueToSize);
if (IsDirectionReversed)
 {
 Canvas.SetTop(rangeElement, (thumbSize.Height * 0.5) + Math.Max(SelectionStart - Minimum, 0) * valueToSize);
 }
else
 {
 Canvas.SetTop(rangeElement, (thumbSize.Height * 0.5) + Math.Max(Maximum - SelectionEnd,0) * valueToSize);
 }
 }
}
           
Since the logic of Orientation (horizontal/vertical) is practically the same, we only need to focus on the logic of the horizontal direction.

It is through this (UpdateSelectionRangeElementPositionAndSize) method that determines the size and position of the SelectionRange. Although the amount of source code may seem like a lot, it's easy to see that the handling of SelectionRange is relatively concise, given the duplication of the Orientation branching logic.

In this way, we can look up and analyze the CustomControl control by reversing it as well

PART_

How controls are handled internally.

8. OnApplyTemplate in Cross-Platform

Many cross-platform frameworks share many similarities to WPF in design, so they follow a similar pattern in their processes. So, we can take a look at implementations on other platforms based on the OnApplyTemplate we analyzed earlier.

List of platforms to share OnApplyTemplate designs:

  • [x] AvaloniaUI
  • [x] Uno Platform
  • [x] OpenSilver
  • [x] MAUI
  • [x] Xamarin
  • [ ] UWP
  • [ ] WinUI 3
  • [ ] Silverlight

Among these platforms, the original source code for AvaloniaUI, Uno Platform, OpenSilver, MAUI, and Xamarin, which has been checked, is worth looking at further.

It is worth mentioning that, with the exception of Silverlight, the codebases for these platforms are managed on GitHub by the official Microsoft organizations Dotnet or xamarin, so we can easily find them.
Animation: OnApplyTemplate

下面是 AvaloniaUI Song Song 控件的 OnApplyTemplate 部分。

protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
 ...
base.OnApplyTemplate(e);
 _decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
 _track = e.NameScope.Find<Track>("PART_Track");
 _increaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
 ...
}
           
AvaloniaUI is also an open-source project, so you can see all the source code just like WPF. You can see it in a very similar way to WPF.

With this naming convention, we can easily see that these three strips are:

PART_

Prefixed controls exist as required components in XAML. Let's take a look at the implementation of Uno.

Uno Platform: OnApplyTemplate
protected override void OnApplyTemplate()
{
 ... 
base.OnApplyTemplate(e);

// 获取组件
var spElementHorizontalTemplateAsDO = GetTemplateChild("HorizontalTemplate");
 _tpElementHorizontalTemplate = spElementHorizontalTemplateAsDO as FrameworkElement;
var spElementTopTickBarAsDO = GetTemplateChild("TopTickBar");
 ...
}
           
The implementation in Uno is also similar to WPF.

Uno didn't follow suit, though

PART_

Naming conventions. It may be that it was decided not to use such a rule from the beginning.

Of course, we can find similar code in MAUI, OpenSilver, and Xamarin.

MAUI: OnApplyTemplate
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
 _thumb = (Thumb)GetTemplateChild("HorizontalThumb");
 _originalThumbStyle = _thumb.Style;

 UpdateThumbStyle();
}
           
In WPF, we usually use Track when declaring variable names, while in MAUI we use the underscore prefix instead. So comparing the naming conventions and development patterns of various platforms is also a very interesting thing in open source.
OpenSilver: OnApplyTemplate
public override void OnApplyTemplate()
{
base.OnApplyTemplate();

// 获取组件
 ...
 ElementVerticalThumb = GetTemplateChild(ElementVerticalThumbName) as Thumb;
 ...
}
           
Seems to use a similar annotation style to Uno.
Xamarin: OnApplyTemplate
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
 FormsContentControl = Template.FindName("PART_Multi_Content", this) 
as FormsTransitioningContentControl;
}
           
Although slightly different, they all share a design similar to WPF.

9. Summarize the Slider analysis

We analyzed WPF's Slider control in detail. From this analysis, we can confirm that WPF's (CustomControl) control design is very sophisticated. These rules apply to other controls as well, and are a very important foundation when designing new controls.

Some people think that WPF is dead, but in fact WPF still exists and continues to evolve. Diving into WPF opens up endless possibilities and fun.

In the past, we may have dreamed of doing all of our development with WPF, but with the advent of platforms like Xamarin and .NET Core, that dream has become a reality. It's the result of the efforts of many developers who love WPF.

Through this analysis, we understand the importance of basic control analysis. It is recommended that you review and learn these contents again through the tutorial videos.

Next, we'll create a new Riot-style (CustomControl) Slider based on this analysis.

10. Create a Riot-style Slider (CustomControl) control

Now, based on the analysis of Slider, we will take advantage of the characteristics of the controls and implement it with a minimum of design. In this process, the core is to take advantage of the PART_ part and not use any code to complete the control.

motivation

It's not common to use the default Slider directly, so we need inspiration. As it happens, I tried to design a Slider based on Riot Games' League of Legends game, so I decided to use that as inspiration for the controls.

Actually, this design started a few years ago when I wanted to implement a high-level game client with WPF and started the development of the League of Legends application. So, if you want to see this Slider widget in action, check out this repository. Anyone can contribute through Fork, and there have been more than 80 forks recorded so far.

League of Legends style slider with WPF |

So let's start creating a new (CustomControl) Slider control.

11. Create and launch the project

Now that you've created your DemoApp (WPF application) project earlier, it's time to create a CustomControl library project. If you want to continue in the DemoApp project, you can skip the project creation process.

Project Creation:
  • [x] Project name: SliderControl
  • [x] 项目类型:WPF CustomControl Library
  • [x] Project version: .NET 8.0
League of Legends style slider with WPF |
To delete the underlying file:
  • [x] AssemblyInfo.cs
  • [x] Themes/Generic.xaml
  • [x] CustomControl1.cs
League of Legends style slider with WPF |

These deleted files are actually required to make up the CustomControl control, but we delete them in order to reorganize the project structure or location.

During the process of recreating the control, the deleted elements are automatically regenerated, so there is no need to worry about file deletion.
Create a (CustomControl) file:
  • [x] 创建RiotSlider.cs (CustomControl)Class
League of Legends style slider with WPF |

DefaultStyleKeyProperty related statements are included with the static constructor only if the file type is set to the CustomControl class. If you select the wrong type during the creation process, the CustomControl-related code will be missed and will need to be entered manually, so be sure to check each step carefully.

public class RiotSlider : Slider
{
static RiotSlider()
 {
 DefaultStyleKeyProperty.OverrideMetadata(typeof(RiotSlider), new FrameworkPropertyMetadata(typeof(RiotSlider)));
 }
}
           
Confirm the auto-generated file:
  • [x] Properties/AssemblyInfo.cs
  • [x] Themes/Generic.xaml
League of Legends style slider with WPF |

If you don't set the file type to the CustomControl class, these files won't be automatically generated either. This is important to note.

12. TextBlock (Hi Slider)

Next is the step to test that the Slider control has been correctly configured to the CustomControl format.

When you first create a CustomControl Slider widget, an empty ControlTemplate template is generated by default. So, in order to visually confirm the controls, we usually add some design elements. We'll add a temporary TextBlock and text here.

添加临时TextBlock:
  • [x] Hi Slider
<Style TargetType="{x:Type local:RiotSlider}">
 <Setter Property="Template">
 <Setter.Value> 
 <ControlTemplate TargetType="{x:Type RiotSlider}">
 <Border Background="{TemplateBinding Background}"
 BorderBrush="{TemplateBinding BorderBrush}"
 BorderThickness="{TemplateBinding BorderThickness}">
 <TextBlock Text="Hi Slider" Foreground="Blue"/>
 </Border>
 </ControlTemplate>
 </Setter.Value>
 </Setter>
</Style>
           
Add the TextBlock and "Hi Slider" text to the Border of the empty ControlTemplate. You can also change the font color.

13. Add references and test runs

Once you're ready to test your TextBlock, it's time to run the DemoApp app and check if the RiotSlider controls load correctly.

To add a reference to a DemoApp project:
  • [x] Added reference: RiotSliderControl project
Declare xmlns in MainWindow.xaml and add controls:
  • [x] 声明xmlns:xmlns:riots
  • [x] 插入控件:riots:RiotSlider
<Window x:Class="DemoApp.MainWindow" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:riots="clr-namespace:SliderControl;assembly=SliderControl"
mc:ignorable="d"
Title="MainWindow" Width="800" Height="450">
<Grid>
<riots:RiotSlider/>
</Grid>
</Window>
           

This will allow us to view and test the RiotSlider controls we have created in the DemoApp app.

Check the running result:
  • [x] Riot Slider: "Hi Slider"
League of Legends style slider with WPF |

At this point, we've finished configuring the (CustomControl) RiotSlider widget and confirmed that it is working properly.

Since the CustomControl method is more complex than the UserControl method, you may encounter some difficulties before becoming familiar with the process. Therefore, these difficulties need to be overcome through repetitive exercises.

Now, this RiotSlider widget has been modularized in the form of a CustomControl for easy management. We can upload this control to a GitHub repository for management, or publish it to a NuGet package for distribution. CustomControl modularity in WPF has many advantages in terms of management, so this should be taken into account when designing a project.

Of course, this project has already been released through the NuGet Package Store. Interesting, right?

14. Set the size of the Riot Slider

Next, we'll set the size of the control.

WPF provides very powerful and responsive layouts. As a result, when you specify the size of a control, it is often designed to be responsive. However, for some special controls, such as Slider, which contains many design elements, it may be necessary to set a fixed height or width to achieve a natural design. Therefore, it is important to respond flexibly according to the characteristics of the control.

This time, we're going to design a (Thumb) widget with a height of 50. Therefore, we pre-specify the height of the RiotSlider. In addition, although the width will be responsive as the movement path of the track, we will temporarily limit it to 200 for development convenience.

Adjust the size and color of the controls:
  • [x] Width: 200
  • [x] Height: 50
  • [x] Background color: "#EEEEEE"
<Window x:Class="DemoApp.MainWindow" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression.blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:riots="clr-namespace:SliderControl;assembly=SliderControl"
mc:ignorable="d"
Title="MainWindow" Width="800" Height="450">
<Grid>
<riots:RiotSlider Width="200" Height="50" Background="#EEEEEE"/>
</Grid>
</Window>
           
To better estimate the size of the control, you can temporarily change the background color to make it easier to identify the control. It's a little trick.
Check the running result:
  • [x] Check the control size: width/height
  • [x] Check the control color: background color
League of Legends style slider with WPF |

After confirming that there are no problems with the running results, you can remove the background color.

15. PART_Track

Track is the core control element of the Slider that includes the Thumb. Through the analysis, we can see the pass

PART_Track

Declared, the Slider control can handle all of these features. Therefore, it is very important to include this key element correctly in the implementation process.

Let's take a closer look.

Add Tracks:
  • [x] Insert PART_Track control element
<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Track x:Name="PART_Track"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           
Track is one of the few controls that inherits directly from the FrameworkElement instead of Control. This means that it doesn't qualify for designing layouts directly like ControlTemplate does. As such, it contains Thumb and builds the layout directly, so it can be assumed that this control is primarily focused on Thumb.
Define Thumb:

Next, we define the Thumb that moves on the Track.

  • [x] 扩展Thumb并定义模板
  • [x] 实现Ellipse
<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           
This is an example of extending directly in Track and implementing Thumb. The grammar may be a little difficult to understand, but there are detailed visual explanations in the tutorial videos, and it is recommended that you watch the instructional videos.

In this Thumb example, defining a control via a template is different from a Track. This means that the Thumb inherits from the Control instead of the FrameworkElement, so the controls can be designed flexibly via the ControlTemplate.

Check the running result:
  • [x] 检查Thumb (Ellipse) 设计
  • [x] Check the Track movement function
League of Legends style slider with WPF |

Since the Thumb is designed to be an Ellipse shape, this large (50x50) ellipse moves within the Track area. However, if you change the name of the Track from

PART_Track

Change to a different name and the Thumb will not be able to move.

To reaffirm this relevance, you can try changing the name.

16. Add a slider bar

Next, let's add a slider. This step is just to add design elements and doesn't involve functionality. So omitting this step will not affect the functionality. However, since the next step is the SelectionRange phase and needs to incorporate design elements, this step also needs to be done carefully.

To modify the layout:

So far, there were only Track elements inside Border, but now you need to add a slider bar, so you'll need to modify the existing layout. Also, because Tracks and slider bars require an overlay effect, using Grid is the only option. So, start by wrapping the Track in the Grid.

  • [x] Modify layout: Use Grid
<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           

Because you only need a simple overlay effect, you don't need to use Grid's RowDefinitions or ColumnDefinitions.

To add a slider bar that overlays with a Track:

The slider bar needs to be stacked with the Track, but it needs to be logically determined which element is in front (frontend) first. Because the Track's Thumb control needs to cover the slider bar area, you need to add the slider bar before you declare the track.

  • [x] 添加:(Border)滑块条
  • [x] Height: 2.5
  • [x] Background color: #CCCCCC
<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Border Background="#CCCCCC" Height="2.5"/>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           

In addition, because you need to visually represent the length of the track, it is effective to use a layout like Border. In particular, Border's CornerRadius property can handle the effect of rounded corners, which can express richer designs than other controls.

Confirmation of operation results:
  • [x] 确认 Thumb 的移动: (Ellipse)
  • [x] 确认滑块条设计: (Border)
League of Legends style slider with WPF |

As you can see, the design and position of the slider bar should be in harmony with the movement path of the Track and the movement of the Thumb, which is the key point in this phase.

17. Adjust the error between the slider bar and the track

While the design and placement of the slider bar looks good, the track's range of movement actually limits the radius of the Thumb at the start and end. Looking at WPF's source code, we can see the following code:

Canvas.SetLeft(rangeElement, (thumbSize.Width * 0.5) + Math.Max(Maximum - SelectionEnd, 0) * valueToSize);
           
The above code is for the case of Orientation="Horizontal". Therefore, if the value changes to vertical, you need to change it to Height.

From this code, we can deduce that the actual range of movement of the Track is also internally limited by the radius of the ThumbSize. As a result, the slider bar we added earlier is not managed internally by the Slider control

PART_

element, so you need to apply this rule manually. While this can be handled dynamically, in this step we use the Margin property to precisely adjust the error between the slider bar and the track movement range.

设置 Thumb Ellipse 的透明度:

To make it easier to work, specify the transparency of the Ellipse control.

  • [x] Ellipse 填充颜色: #55000000
<Ellipse Width="50" Height="50" Fill="#55000000"/>
           
In WPF, an object's transparency property, Opacity, is typically used when setting element transparency, but by using a color's alpha value, you can apply transparency to only a specific color. Here's a little trick from WPF that is well worth taking advantage of.
Apply the Margin of the slider bar to account for the radius of the Thumb:

The current width of the Ellipse is 50, so a margin of 25 is applied to the left and right.

  • [x] Margin="25 0 25 0"
<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Border Background="#CCCCCC" Height="2.5" Margin="25 0 25 0"/>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#55000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           
Confirmation of results:
  • [x] 确认 Thumb 半径和 Margin尺寸
League of Legends style slider with WPF |

As you can see, the maximum range of movement of the track and the design dimensions of the slider bar are exactly the same.

Of course, you can also consider the dynamic processing of this Sync operation later. One approach that comes to mind so far is to specify this slider bar control as

PART_

and then process it in CodeBehind. Of course, there are various other ways to do this, and it's worth thinking about.

18. PART_SelectionRange

The SelectionRange is an element that is responsible for specifying a specific Range range through the previous Slider analysis.

This control is the same as Track

PART_

element, the Slider control handles all of its functions internally, so it just needs to be placed correctly according to the agreed name. Since it is designed to be the same height as the slider bar, the height should be the same as the slider bar added earlier.

添加 SelectionRange Border 区域:
  • [x] Name:

    PART_SelectionRange

  • [x] Height: 2.5
  • [x] Background color: #000000
  • [x] Margin: 25 0 25 0
<Border x:Name="PART_SelectionRange" 
Background="#000000" 
Height="2.5"
Margin="25 0 25 0"/>
           
Specify the Range range:

通过 RelativeSource Binding 将 SelectionEnd 的范围与 Value 值同步。

  • [x] SelectionStart: 0
  • [x] SelectionEnd: {Binding RelativeSource {RelativeSource Self}, Path=Value}
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
           

By synchronizing the value of SelectionEnd with the value of Value, you can dynamically express the Range range. Actually, the Slider control of the League of Legends client app is implemented in the same way.

Full code:

Combining the above steps, the complete code is as follows:

<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Border x:Name="PART_SelectionRange" 
Background="#000000" 
Height="2.5"
Margin="25 0 25 0"/>
<Border Background="#CCCCCC" Height="2.5" Margin="25 0 25 0"/>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#55000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           

With that, we've done the addition of the SelectionRange and the synchronization of the slider bar with the Track.

处理 IsSelectionRangeEnabled 启用:

Considering the concept of Riot Slider controls, there may not be a need to deal with this feature. But since it can be easily achieved with triggers, we can try it out as we learn.

This section is not covered in the tutorial video.
  • [x] IsSelectionRangeEnabled: True
<Setter Property="IsSelectionRangeEnabled" Value="True"/>
           
将 IsSelectionRangeEnabled 属性的默认值设置为 True。
  • [x] PART_SelectionRange Visibility: (默认) Collapsed
<Border x:Name="PART_SelectionRange" 
Background="#000000" 
Height="2.5"
Margin="25 0 25 0"
Visibility="Collapsed"/>
           
将 SelectionRange 的默认 Visibility 设置为 Collapsed。
  • [x] 触发器: PART_SelectionRange.Visibility=Visible
<Trigger Property="IsSelectionRangeEnabled" Value="True">
<Setter TargetName="PART_SelectionRange" Property="Visibility" Value="Visible"/>
</Trigger>
           
将 SelectionRange 的默认 Visibility 设置为 Collapsed,当 IsSelectionRangeEnabled 属性的值为 True 时,通过触发器将 Visibility 设置为 Visible。 虽然可以反向设置,但在触发器中检查 Boolean 属性的 True 值更符合常规代码习惯。
Source code and operation result confirmation:
  • [x] 应用 Setter
  • [x] SelectionRange (默认) Collapsed
  • [x] 应用触发器 IsSelectionRangeEnabled
<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="IsSelectionRangeEnabled" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Border Background="#CCCCCC" Height="2.5" Margin="25 0 25 0"/>
<Border x:Name="PART_SelectionRange" 
Background="#000000" 
Height="2.5"
Margin="25 0 25 0"
HorizontalAlignment="Left"
Visibility="Collapsed"/>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#55000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelectionRangeEnabled" Value="True">
<Setter TargetName="PART_SelectionRange" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           
League of Legends style slider with WPF |

Now, we've added all the elements that make up the Slider feature. Next, we'll check again

PART_

control element to complete this step and move on to the next stage.

Reconfirm the functionality of the PART_ control:
  • [x] PART_Track
  • [x] PART_SelectionRange

19. Add Riot-style design elements

Next is to add the desired design elements for Riot Slider.

League of Legends style slider with WPF |
To add a Geometry design asset:
  • [x] Geometry: ThumbData
<Geometry x:Key="ThumbData">
 M12 2C11.5 2 11 2.19 10.59 2.59L2.59 10.59C1.8 11.37 1.8 12.63 2.59 13.41L10.59 21.41C11.37 22.2 12.63 22.2 13.41 21.41L21.41 13.41C22.2 12.63 22.2 11.37 21.41 10.59L13.41 2.59C13 2.19 12.5 2 12 2M12 4L15.29 7.29L12 10.59L8.71 7.29L12 4M7.29 8.71L10.59 12L7.29 15.29L4 12L7.29 8.71M16.71 8.71L20 12L16.71 15.29L13.41 12L16.71 8.71M12 13.41L15.29 16.71L12 20L8.71 16.71L12 13.41Z
</Geometry>
           

We use the Geometry Path element to draw the Thumb icon instead of the image file, because it has the advantage of being able to change colors freely with color triggers and maintaining high vector-based quality.

This simple icon can be easily made by non-designers with Visual Studio Blend or tools like Figma or Illustrator. It's not difficult, it's worth a try.

When requesting vector icons from colleagues, it is recommended to use the SVG format and require the monochrome design of the icons to be a composition. In addition, there are plenty of open-source icons that are free to use. For example, Pictogrammers offers over 8000 monochrome design icons, including:

.SVG

.PNG

and

.XAML

Format. And with GitHub open source management, you can view major contributors or participate in open source projects.

The next step is to add the main color resource.

添加 LinearGradientBrush 设计资源:
  • [x] LinearGradientBrush: ThumbColor
  • [x] LinearGradientBrush: ThumbOver
  • [x] LinearGradientBrush: ThumbDrag
  • [x] SolidColorBrush: SliderColor
  • [x] LinearGradientBrush: RangeColor
  • [x] LinearGradientBrush: SliderOver
  • [x] LinearGradientBrush: SliderDrag
<LinearGradientBrush x:Key="ThumbColor" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#B79248" Offset="0"/>
<GradientStop Color="#997530" Offset="0.5"/>
<GradientStop Color="#74592B" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="ThumbOver" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#EDE1C8" Offset="0"/>
<GradientStop Color="#DCC088" Offset="0.5"/>
<GradientStop Color="#CBA14A" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="ThumbDrag" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#473814" Offset="0"/>
<GradientStop Color="#57421B" Offset="0.5"/>
<GradientStop Color="#684E23" Offset="1"/>
</LinearGradientBrush>

<SolidColorBrush x:Key="SliderColor" Color="#1E2328"/>

<LinearGradientBrush x:Key="RangeColor" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="#463714" Offset="0"/>
<GradientStop Color="#58471D" Offset="0.5"/>
<GradientStop Color="#695625" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="SliderOver" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="#795B28" Offset="0"/>
<GradientStop Color="#C1963B" Offset="0.5"/>
<GradientStop Color="#C8AA6D" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="SliderDrag" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="#685524" Offset="0"/>
<GradientStop Color="#55441B" Offset="0.5"/>
<GradientStop Color="#463714" Offset="1"/>
</LinearGradientBrush>
           
x:Key rules for design resources such as colors often have uppercase or camel nomenclature, as well as namespace-like approaches. Personal advice is to be as brief as possible. While perceptions vary from year to year, there is a current tendency to keep things as brief as possible.

A closer look at the design style of League of Legends reveals a lot of use of gradients. The way to extract these colors is to use Photoshop or an app with an eyedropper color extraction feature.

For areas where gradients may be present, the eyedropper function can be used multiple times to extract the color. With a lot of practice, your eyes will also become sharper.
League of Legends style slider with WPF |

20. Implement a Riot-style Thumb

Now, we'll use the prepared Geometry and design elements to officially create a League of Legends-style Thumb control.

Before we begin, we need to delete the temporary Ellipse that we defined earlier as a template for the Thumb. So, remove all parts of the Thumb definition that contain Ellipse.

To delete an existing thumb:
  • [x] 删除 Thumb 及其模板
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="50" Height="50" Fill="#55000000"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Track.Thumb>
</Track>
           
删除直接在 Track 内定义的 Thumb 和模板,只保留 Track。

Now is the time to create a new Riot-style Thumb.

The Thumb we just removed was a temporary template that was defined directly by extending the Track, but this time we'll implement it with neat resource management via StaticResource.

Define a new Thumb template:
  • [x] Implement a Riot-style thumb and refine resources
<Style TargetType="{x:Type Thumb}" x:Key="ThumbStyle">
<Setter Property="Background" Value="#010A13"/>
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Grid Background="{TemplateBinding Background}">
<Path x:Name="path" Data="{StaticResource ThumbData}" Fill="{StaticResource ThumbColor}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="path" Property="Fill" Value="{StaticResource ThumbOver}"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="path" Property="Fill" Value="{StaticResource ThumbDrag}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
           
On top of CustomControl, XAML resource management is actually pretty straightforward. Since resources have been physically separated via Generic.xaml, you can continue to manage them at a more granular level with x:Key. That's why we've also separated Geometry from LinearGradientBrush. These assets only need to be included in the style file of the RiotSlider control.

As a control inherited from Control, the Thumb can be designed using a template (ControlTemplate). As a result, you can create a control that implements a detailed trigger. In addition, if you want to create more detailed controls, you can further refine the thumb with CustomControl. This is very common in WPF basic controls.

Explore some knowledge. For example, controls like ToolBarOverflowPanel, although they may sound relatively unfamiliar, are actually quite numerous. These are created when more granular controls are needed on top of CustomControl, and are typically categorized in the Primitives namespace.

As a result, controls that belong to the Primitives namespace are usually included in other (CustomControl) controls. Take the ToggleButton, the representative control of Primitives, for example, which is not only the parent control of CheckBox/RadioButton, but can also be used as a control for toggles in the template of a control such as a ComboBox.

Interesting, right? These architectural concepts apply to all cross-platform technologies that share XAML. Therefore, being able to flexibly apply these concepts can also be very helpful in environments such as AvaloniaUI, Uno, MAUI, etc.
Of course, not all controls that belong to the Primitives namespace are specified as CustomControl via the DefaultStyleKey. There are also many classes that are simply encapsulated.

21. 声明 Thumb 资源

Finally, declare the Thumb as a resource for use in the Track via StaticResource.

To add a Thumb resource:
  • [x] Define the Thumb style that contains the template with the Thumb resource connection
<Thumb x:Key="SliderThumb" Style="{StaticResource ThumbStyle}"/>
           
This part is described in detail in the tutorial video, if the syntax does not feel very natural, it is recommended to refer to the video.

Now, all you need to do is use the resourced thumb in the track.

Define a thumb succinctly in a track:
  • [x] 使用 StaticResource 连接替换现有 Thumb
<Track Thumb="{StaticResource SliderThumb}"/>
           
Using a Thumb in the form of a Resource can greatly reduce the amount of code when defining a Thumb in a Track. In addition, managing resources in this way helps to have a clearer grasp of resources as a whole and is one of the important ways to maintain the quality of your code. Therefore, it is necessary to carefully study this management method.

Proceed to the next step: Check them all

PART_

Whether the control is functioning properly.

Double-confirm

PART_

Widget Functions:
  • [x] PART_Track
  • [x] PART_SelectionRange

22. Completion RiotSlider Template (Wrap Up)

Now we'll be done with the template implementation of the RiotSlider control. In addition to this, the Jamesnet.WPF library is included, so we use JamesGrid, which can also be replaced by normal Grid.

(CustomControl) RiotSlider:
  • [x] 查看 Generic.xaml 的完整源代码
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:james="https://jamesnet.dev/xaml/presentation"
xmlns:local="clr-namespace:SliderControl">

<Geometry x:Key="ThumbData">
 M12 2C11.5 2 11 2.19 10.59 2.59L2.59 10.59C1.8 11.37 1.8 12.63 2.59 13.41L10.59 21.41C11.37 22.2 12.63 22.2 13.41 21.41L21.41 13.41C22.2 12.63 22.2 11.37 21.41 10.59L13.41 2.59C13 2.19 12.5 2 12 2M12 4L15.29 7.29L12 10.59L8.71 7.29L12 4M7.29 8.71L10.59 12L7.29 15.29L4 12L7.29 8.71M16.71 8.71L20 12L16.71 15.29L13.41 12L16.71 8.71M12 13.41L15.29 16.71L12 20L8.71 16.71L12 13.41Z
</Geometry>

<LinearGradientBrush x:Key="ThumbColor" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#B79248" Offset="0"/>
<GradientStop Color="#997530" Offset="0.5"/>
<GradientStop Color="#74592B" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="ThumbOver" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#EDE1C8" Offset="0"/>
<GradientStop Color="#DCC088" Offset="0.5"/>
<GradientStop Color="#CBA14A" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="ThumbDrag" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Color="#473814" Offset="0"/>
<GradientStop Color="#57421B" Offset="0.5"/>
<GradientStop Color="#684E23" Offset="1"/>
</LinearGradientBrush>

<Style TargetType="{x:Type Thumb}" x:Key="ThumbStyle">
<Setter Property="Background" Value="#010A13"/>
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Grid Background="{TemplateBinding Background}">
<Path x:Name="path" Data="{StaticResource ThumbData}" Fill="{StaticResource ThumbColor}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="path" Property="Fill" Value="{StaticResource ThumbOver}"/>
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="path" Property="Fill" Value="{StaticResource ThumbDrag}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Thumb x:Key="SliderThumb" Style="{StaticResource ThumbStyle}"/>

<SolidColorBrush x:Key="SliderColor" Color="#1E2328"/>

<LinearGradientBrush x:Key="RangeColor" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="#463714" Offset="0"/>
<GradientStop Color="#58471D" Offset="0.5"/>
<GradientStop Color="#695625" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="SliderOver" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="#795B28" Offset="0"/>
<GradientStop Color="#C1963B" Offset="0.5"/>
<GradientStop Color="#C8AA6D" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="SliderDrag" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Color="#685524" Offset="0"/>
<GradientStop Color="#55441B" Offset="0.5"/>
<GradientStop Color="#463714" Offset="1"/>
</LinearGradientBrush>

<Style TargetType="{x:Type local:RiotSlider}">
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="100"/>
<Setter Property="SelectionStart" Value="0"/>
<Setter Property="SelectionEnd" Value="{Binding RelativeSource={RelativeSource Self}, Path=Value}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RiotSlider}">
<Grid Background="{TemplateBinding Background}">
<james:JamesGrid Rows="*" Columns="Auto,*" Height="2.5" Margin="12 0 12 0">
<Border Background="{StaticResource RangeColor}" x:Name="PART_SelectionRange"/>
<Border Background="{StaticResource SliderColor}"/>
</james:JamesGrid>
<Track x:Name="PART_Track" Thumb="{StaticResource SliderThumb}"/>
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=PART_Track, Path=Thumb.IsMouseOver}" Value="True">
<Setter TargetName="PART_SelectionRange" Property="Background" Value="{StaticResource SliderOver}"/>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=PART_Track, Path=Thumb.IsDragging}" Value="True">
<Setter TargetName="PART_SelectionRange" Property="Background" Value="{StaticResource SliderDrag}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
           
In addition, two triggers have been added and the (ControlTemplate) template area of the RiotSlider control has been broken down into resources to manage all elements at a glance, which is a feature of this project.

Because the Slider control is implemented on top of (CustomControl), related resources can be managed in the same way as resource packages.

Confirm the final result:
  • [x] Testing

    PART_Track

    Related features
  • [x] Testing

    PART_SelectionRange

    Related features
  • [x] Confirm the app design elements
Although the functional part has gone through several stages of analysis and implementation, it is still in the form of:

PART_

The control is standard and checks the function again.
League of Legends style slider with WPF |

This completes the detailed explanation of the CustomControl-based development process and a review of the tutorial video, from analyzing the basic Slider controls to implementing the League of Legends-style RiotSlider controls.

23. Final Words

We took a deep dive into the WPF Slider controls from an architectural perspective. On the surface, it seems simple, but there's actually a lot to discuss, which shows how much you can learn when it comes to design with WPF. We also recommend that you watch our tutorial video, where we show you coding and explain it in detail.

WPF is a platform with a bit of history, so over the years, various development methodologies, frameworks, and component open-source libraries have evolved and changed. Mainstream evaluations and interpretations change over time. As a result, the historical experience accumulated so far can actually be the cornerstone of our technology. If we have the flexibility to judge and evaluate these histories, we can find more rich and high-quality references. The mainstream view is not always true.

This is a long review that we have written with heart for a long time, and we hope to pass it on to more people.

Thank you!