You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF中如何为ScrollBar的Thumb设置自定义PNG图片?

自定义ScrollBar的Thumb无法显示问题排查与修复

问题描述

我正在开发一款具有特定GUI风格的应用,应用中的TreeView带有ScrollBar,但默认滚动条样式过于基础,无法匹配应用使用的UI纹理。我尝试为ScrollBar的Thumb设置自定义PNG图片,箭头按钮已成功显示自定义图片,但Thumb始终无法正常显示。以下是我使用的XAML和C#代码:


原XAML代码

<Window x:Class="TestAPP.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestAPP"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!-- Register ScrollThumbConverter -->
        <local:ScrollThumbConverter x:Key="ScrollThumbConverter"/>
        <local:ThumbHeightConverter x:Key="ThumbHeightConverter" />

        <!-- Custom ScrollBar Style -->
        <Style x:Key="CustomScrollBar" TargetType="{x:Type ScrollBar}">
            <Setter Property="Width" Value="15"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ScrollBar}">
                        <Grid Background="Transparent" Margin="0,0,0,116">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>

                            <!-- Up Arrow -->
                            <RepeatButton Grid.Row="0" Height="15" Width="15"
                                          Click="UpArrow_Click"
                                          BorderThickness="0">
                                <RepeatButton.Background>
                                    <ImageBrush ImageSource="/154965.png" Stretch="Fill"/>
                                </RepeatButton.Background>
                            </RepeatButton>

                            <!-- ScrollBar Track with MultiBinding -->
                            <Track Grid.Row="1" Name="PART_Track">
                                <Track.Thumb>
                                    <Thumb Height="50" Width="15" BorderThickness="0" Padding="0">
                                        <Thumb.Template>
                                            <ControlTemplate TargetType="{x:Type Thumb}">
                                                <Grid>
                                                    <Image Source="/154971.png" Stretch="Fill"/>
                                                </Grid>
                                            </ControlTemplate>
                                        </Thumb.Template>

                                        <!-- MultiBinding for thumb position -->
                                        <Thumb.RenderTransform>
                                            <MultiBinding Converter="{StaticResource ScrollThumbConverter}">
                                                <Binding RelativeSource="{RelativeSource AncestorType=ScrollViewer}" Path="VerticalOffset" />
                                                <Binding RelativeSource="{RelativeSource AncestorType=ScrollViewer}" Path="ScrollableHeight" />
                                                <Binding RelativeSource="{RelativeSource AncestorType=ScrollViewer}" Path="ViewportHeight" />
                                            </MultiBinding>
                                        </Thumb.RenderTransform>
                                    </Thumb>
                                </Track.Thumb>
                            </Track>

                            <!-- Down Arrow -->
                            <RepeatButton Grid.Row="2" Height="15" Width="15"
                                          Click="DownArrow_Click"
                                          BorderThickness="0">
                                <RepeatButton.Background>
                                    <ImageBrush ImageSource="/154964.png" Stretch="Fill"/>
                                </RepeatButton.Background>
                            </RepeatButton>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid>
        <!-- ScrollViewer with custom scrollbar -->
        <ScrollViewer x:Name="MyScrollViewer"
                      VerticalScrollBarVisibility="Visible"
                      HorizontalScrollBarVisibility="Disabled"
                      ScrollChanged="ScrollViewer_ScrollChanged">
            <ScrollViewer.Style>
                <Style TargetType="ScrollViewer">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ScrollViewer">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition/>
                                        <ColumnDefinition Width="Auto"/>
                                    </Grid.ColumnDefinitions>
                                    <ScrollContentPresenter Grid.Column="0"/>
                                    <ScrollBar Grid.Column="1" Style="{StaticResource CustomScrollBar}" Orientation="Vertical"/>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ScrollViewer.Style>

            <!-- Example content to scroll -->
            <StackPanel>
                <TextBlock Text="Line 1" Height="50"/>
                <TextBlock Text="Line 2" Height="50"/>
                <TextBlock Text="Line 3" Height="50"/>
                <TextBlock Text="Line 4" Height="50"/>
                <TextBlock Text="Line 5" Height="50"/>
                <TextBlock Text="Line 6" Height="50"/>
                <TextBlock Text="Line 7" Height="50"/>
                <TextBlock Text="Line 8" Height="50"/>
                <TextBlock Text="Line 9" Height="50"/>
                <TextBlock Text="Line 10" Height="50"/>
            </StackPanel>
        </ScrollViewer>
    </Grid>
</Window>

原C#代码

using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace TestAPP
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // Scroll up when up arrow is clicked
        private void UpArrow_Click(object sender, RoutedEventArgs e)
        {
            MyScrollViewer.LineUp();  // Scroll one line up
        }

        // Scroll down when down arrow is clicked
        private void DownArrow_Click(object sender, RoutedEventArgs e)
        {
            MyScrollViewer.LineDown();  // Scroll one line down
        }

        // Handle scroll changes (you can manually adjust the thumb size or handle any visual logic here)
        private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            // Optional: Add any additional logic to manage thumb movement or other scroll events.
        }
    }

    public class ScrollThumbConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length < 3 || !(values[0] is double verticalOffset) || !(values[1] is double scrollableHeight) || !(values[2] is double viewportHeight))
            {
                return new TranslateTransform(0, 0); // Default transform
            }

            // Calculate the thumb's position
            double trackHeight = scrollableHeight + viewportHeight; // Total track height
            double ratio = scrollableHeight > 0 ? verticalOffset / scrollableHeight : 0;
            double thumbPosition = ratio * (trackHeight - viewportHeight); // Calculate thumb position

            return new TranslateTransform(0, thumbPosition);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    
    public class ThumbHeightConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length < 2 || !(values[0] is double viewportHeight) || !(values[1] is double scrollableHeight))
            {
                return 50.0; // Default thumb height if values are missing
            }

            // Calculate the thumb height as a ratio of the viewport to the total scrollable height
            double totalHeight = scrollableHeight + viewportHeight;
            double thumbHeight = (viewportHeight / totalHeight) * viewportHeight;

            return Math.Max(20, thumbHeight); // Ensure a minimum thumb height for usability
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

问题原因

  1. Track未绑定核心属性:PART_Track没有绑定ScrollBar的Minimum、Maximum、Value等属性,无法和ScrollBar的滚动状态联动,导致Thumb没有有效显示范围。
  2. 手动控制Thumb位置冲突:通过RenderTransform手动计算位置的逻辑,和WPF原生Track的自动布局逻辑冲突,破坏了Thumb的正常渲染流程。
  3. ThumbHeightConverter未生效:定义了转换器但未在Thumb的Height属性上应用绑定,Thumb高度固定且可能超出轨道可视范围。
  4. ScrollBar与ScrollViewer未联动:自定义ScrollBar没有绑定ScrollViewer的滚动属性,导致两者状态不同步,Thumb无法响应滚动操作。

修复后的代码

XAML代码

<Window x:Class="TestAPP.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestAPP"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:ThumbHeightConverter x:Key="ThumbHeightConverter" />

        <!-- Custom ScrollBar Style -->
        <Style x:Key="CustomScrollBar" TargetType="{x:Type ScrollBar}">
            <Setter Property="Width" Value="15"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ScrollBar}">
                        <Grid Background="Transparent">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>

                            <!-- Up Arrow -->
                            <RepeatButton Grid.Row="0" Height="15" Width="15"
                                          Command="ScrollBar.LineUpCommand"
                                          BorderThickness="0">
                                <RepeatButton.Background>
                                    <ImageBrush ImageSource="/154965.png" Stretch="Fill"/>
                                </RepeatButton.Background>
                            </RepeatButton>

                            <!-- ScrollBar Track -->
                            <Track Grid.Row="1" Name="PART_Track"
                                   Minimum="{TemplateBinding Minimum}"
                                   Maximum="{TemplateBinding Maximum}"
                                   Value="{TemplateBinding Value}"
                                   ViewportSize="{TemplateBinding ViewportSize}">
                                <Track.DecreaseRepeatButton>
                                    <RepeatButton Command="ScrollBar.PageUpCommand"
                                                  Background="Transparent"
                                                  BorderThickness="0"/>
                                </Track.DecreaseRepeatButton>
                                <Track.IncreaseRepeatButton>
                                    <RepeatButton Command="ScrollBar.PageDownCommand"
                                                  Background="Transparent"
                                                  BorderThickness="0"/>
                                </Track.IncreaseRepeatButton>
                                <Track.Thumb>
                                    <Thumb BorderThickness="0" Padding="0">
                                        <Thumb.Height>
                                            <MultiBinding Converter="{StaticResource ThumbHeightConverter}">
                                                <Binding RelativeSource="{RelativeSource AncestorType=ScrollViewer}" Path="ViewportHeight" />
                                                <Binding RelativeSource="{RelativeSource AncestorType=ScrollViewer}" Path="ScrollableHeight" />
                                            </MultiBinding>
                                        </Thumb.Height>
                                        <Thumb.Template>
                                            <ControlTemplate TargetType="{x:Type Thumb}">
                                                <Grid>
                                                    <Image Source="/154971.png" Stretch="Fill"/>
                                                </Grid>
                                            </ControlTemplate>
                                        </Thumb.Template>
                                    </Thumb>
                                </Track.Thumb>
                            </Track>

                            <!-- Down Arrow -->
                            <RepeatButton Grid.Row="2" Height="15" Width="15"
                                          Command="ScrollBar.LineDownCommand"
                                          BorderThickness="0">
                                <RepeatButton.Background>
                                    <ImageBrush ImageSource="/154964.png" Stretch="Fill"/>
                                </RepeatButton.Background>
                            </RepeatButton>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid>
        <!-- ScrollViewer with custom scrollbar -->
        <ScrollViewer x:Name="MyScrollViewer"
                      VerticalScrollBarVisibility="Visible"
                      HorizontalScrollBarVisibility="Disabled">
            <ScrollViewer.Style>
                <Style TargetType="ScrollViewer">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ScrollViewer">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition/>
                                        <ColumnDefinition Width="Auto"/>
                                    </Grid.ColumnDefinitions>
                                    <ScrollContentPresenter Grid.Column="0"/>
                                    <ScrollBar Grid.Column="1" 
                                               Style="{StaticResource CustomScrollBar}" 
                                               Orientation="Vertical"
                                               Value="{TemplateBinding VerticalOffset}"
                                               Maximum="{TemplateBinding ScrollableHeight}"
                                               ViewportSize="{TemplateBinding ViewportHeight}"/>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ScrollViewer.Style>

            <!-- Example content to scroll -->
            <StackPanel>
                <TextBlock Text="Line 1" Height="50"/>
                <TextBlock Text="Line 2" Height="50"/>
                <TextBlock Text="Line 3" Height="50"/>
                <TextBlock Text="Line 4" Height="50"/>
                <TextBlock Text="Line 5" Height="50"/>
                <TextBlock Text="Line 6" Height="50"/>
                <TextBlock Text="Line 7" Height="50"/>
                <TextBlock Text="Line 8" Height="50"/>
                <TextBlock Text="Line 9" Height="50"/>
                <TextBlock Text="Line 10" Height="50"/>
            </StackPanel>
        </ScrollViewer>
    </Grid>
</Window>

C#代码

using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace TestAPP
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class ThumbHeightConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length < 2 || !(values[0] is double viewportHeight) || !(values[1] is double scrollableHeight))
            {
                return 50.0; // Default thumb height if values are missing
            }

            double totalHeight = scrollableHeight + viewportHeight;
            double thumbHeight = (viewportHeight / totalHeight) * viewportHeight;

            return Math.Max(20, thumbHeight); // Ensure minimum height for usability
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

关键修复点

  1. 移除手动位置计算:删除ScrollThumbConverter和Thumb的RenderTransform绑定,让Track自动管理Thumb位置。
  2. 绑定核心属性:将Track和ScrollBar的Minimum、Maximum、Value等属性绑定,同时关联ScrollViewer的滚动状态,实现双向联动。
  3. 启用高度转换器:在Thumb的Height属性上应用ThumbHeightConverter,让Thumb高度随内容长度自动调整。
  4. 使用内置命令:箭头按钮改用WPF内置的滚动命令,替代手动点击事件,简化代码逻辑。
  5. 完善Track结构:添加Track的增减按钮(设为透明),恢复点击轨道空白区域翻页的原生交互。

内容的提问来源于stack exchange,提问作者Ferg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 21:58:09