[WPF自定义控件库]使用TextBlockHighlightSource强化高亮的功能,以及使用TypeConverter简化调用...

本文介绍了一种增强WPF TextBlock高亮显示功能的方法,通过自定义类TextBlockHighlightSource,实现了更丰富的颜色配置。同时,利用TypeConverter简化了XAML中的调用,使其如同操作字符串般简便。此外,通过继承FrameworkElement,使得TextBlockHighlightSource能够使用全局Style,便于统一颜色设置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 强化高亮的功能

上一篇文章介绍了使用附加属性实现TextBlock的高亮功能,但也留下了问题:不能定义高亮(或者低亮)的颜色。为了解决这个问题,我创建了TextBlockHighlightSource这个类,比单纯的字符串存储更多的信息,这个类的定义如下:

38937-20190625140036869-236657693.png

相应地,附加属性的类型也改变为这个类,并且属性值改变事件改成这样:

private static void OnHighlightTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    var oldValue = (TextBlockHighlightSource)args.OldValue;
    var newValue = (TextBlockHighlightSource)args.NewValue;
    if (oldValue == newValue)
        return;

    void OnPropertyChanged(object sender,EventArgs e)
    {
        if (obj is TextBlock target)
        {
            MarkHighlight(target, newValue);
        }
    };

    if(oldValue!=null)
        newValue.PropertyChanged -= OnPropertyChanged;

    if (newValue != null)
        newValue.PropertyChanged += OnPropertyChanged;

    OnPropertyChanged(null, null);
}

MarkHighlight的关键代码修改为这样:

if (highlightSource.LowlightForeground != null)
    run.Foreground = highlightSource.LowlightForeground;

if (highlightSource.HighlightForeground != null)
    run.Foreground = highlightSource.HighlightForeground;

if (highlightSource.HighlightBackground != null)
    run.Background = highlightSource.HighlightBackground;

使用起来就是这样:

<TextBlock Text="Git hub"
           TextWrapping="Wrap">
    <kino:TextBlockService.HighlightText>
        <kino:TextBlockHighlightSource Text="hub"
                                       LowlightForeground="Black"
                                       HighlightBackground="#FFF37D33" />
    </kino:TextBlockService.HighlightText>
</TextBlock>

38937-20190625203934908-1613451686.png

2. 使用TypeConverter简化调用

TextBlockHighlightSource提供了很多功能,但和直接使用字符串比起来,创建一个TextBlockHighlightSource要复杂多。为了可以简化调用可以使用自定义的TypeConverter

首先来了解一下TypeConverter的概念。XAML本质上是XML,其中的属性内容全部都是字符串。如果对应属性的类型是XAML内置类型(即Boolea,Char,String,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等类型),XAML解析器直接将字符串转换成对应值赋给属性;对于其它类型,XAML解析器需做更多工作。

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
</Grid.RowDefinitions>

如上面这段XAML中的"Auto"和"*",XAML解析器将其分别解析成GridLength.Auto和new GridLength(1, GridUnitType.Star)再赋值给Height,它相当于这段代码:

grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

为了完成这个工作,XAML解析器需要TypeConverter的协助。XAML解析器通过两个步骤查找TypeConverter:
1. 检查属性声明上的TypeConverterAttribute。
2. 如果属性声明中没有TypeConverterAttribute,检查类型声明中的TypeConverterAttribute。

属性声明上TypeConverterAttribute的优先级高于类型声明。如果以上两步都找不到类型对应的TypeConverterAttribute,XAML解析器将会报错:属性"*"的值无效。找到TypeConverterAttribute指定的TypeConverter后,XAML解析器调用它的object ConvertFromString(string text)函数将字符串转换成属性的值。

WPF内置的TypeConverter十分十分多,但有时还是需要自定义TypeConverter,自定义TypeConverter的基本步骤如下:

  • 创建一个继承自TypeConverter的类;
  • 重写virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
  • 重写virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
  • 重写virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value);
  • 重写virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType);
  • 使用TypeConverterAttribute 指示XAML解析器可用的TypeConverter;

到这里我想TypeConverter的概念已经介绍得够详细了。回到本来话题,要简化TextBlockHighlightSource的调用我创建了TextBlockHighlightSourceConverter这个类,它继承自TypeConverter,里面的关键代码如下:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    switch (value)
    {
        case null:
            throw GetConvertFromException(null);
        case string source:
            return new TextBlockHighlightSource { Text = value.ToString() };
    }

    return base.ConvertFrom(context, culture, value);
}

然后在TextBlockHighlightSource上使用TypeConverterAttribute:

[TypeConverter(typeof(TextBlockHighlightSourceConverter))]
public class TextBlockHighlightSource : FrameworkElement

这样在XAML中TextBlockHighlightSource的调用方式就可以和使用字符串一样简单了。

<TextBlock Text="Github"
           kino:TextBlockService.HighlightText="hub" />

3. 使用Style

有没有发现TextBlockHighlightSource继承自FrameworkElement?这种奇特的写法是为了让TextBlockHighlightSource可以使用全局的Style。毕竟要在应用程序里统一Highlight的颜色还是全局样式最好使,但作为附加属性,TextBlockHighlightSource并不是VisualTree的一部分,它拿不到VisualTree上的Resources。最简单的解决方案是让TextBlockHighlightSource继承自FrameworkElement,把它放到VisualTree里,用法如下:

<StackPanel>
    <FrameworkElement.Resources>
        <Style TargetType="kino:TextBlockHighlightSource">
            <Setter Property="LowlightForeground" Value="Blue"/>
        </Style>
    </FrameworkElement.Resources>
    <TextBox x:Name="FilterElement3"/>
    <kino:TextBlockHighlightSource Text="{Binding ElementName=FilterElement3,Path=Text}" 
                                   HighlightForeground="DarkBlue"
                                   HighlightBackground="Yellow"
                                   x:Name="TextBlockHighlightSource2"/>
    <TextBlock Text="A very powerful projector with special features for Internet usability, USB" 
               kino:TextBlockService.HighlightText="{Binding ElementName=TextBlockHighlightSource2}"
               TextWrapping="Wrap"/>
</StackPanel>

也许你会觉得这种写法有些奇怪,毕竟我也觉得在View上放一个隐藏的元素真的很怪。其实在一万二千年前微软就已经有这种写法,在DomainDataSource的文档里就有用到:

<Grid x:Name="LayoutRoot" Background="White">  
    <Grid.RowDefinitions>
        <RowDefinition Height="25" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <riaControls:DomainDataSource x:Name="source" QueryName="GetProducts" AutoLoad="true">
        <riaControls:DomainDataSource.DomainContext>
            <domain:ProductDomainContext />
        </riaControls:DomainDataSource.DomainContext>   
        <riaControls:DomainDataSource.FilterDescriptors>
            <riaData:FilterDescriptorCollection LogicalOperator="And">
              <riaData:FilterDescriptor PropertyPath="Color" Operator="IsEqualTo" Value="Blue" />
              <riaData:FilterDescriptor PropertyPath="ListPrice" Operator="IsLessThanOrEqualTo">
                  <riaControls:ControlParameter 
                      ControlName="MaxPrice" 
                      PropertyName="SelectedItem.Content" 
                      RefreshEventName="SelectionChanged" />
              </riaData:FilterDescriptor>
            </riaData:FilterDescriptorCollection>
        </riaControls:DomainDataSource.FilterDescriptors>
    </riaControls:DomainDataSource>
    <ComboBox x:Name="MaxPrice" Grid.Row="0" Width="60" SelectedIndex="0">
        <ComboBoxItem Content="100" />
        <ComboBoxItem Content="500" />
        <ComboBoxItem Content="1000" />
    </ComboBox>
    <data:DataGrid Grid.Row="1" ItemsSource="{Binding Data, ElementName=source}" />
</Grid>

把DataSource放到View上这种做法可能是WinForm的祖传家训,结构可耻但有用。

4. 结语

写这篇博客的时候我才发觉这个附加属性还叫HighlightText好像不太好,但也懒得改了。

这篇文章介绍了使用TypeConverter简化调用,以及继承自FrameworkElement以便使用Style。

5. 参考

TypeConverter 类
TypeConverters 和 XAML
Type Converters for XAML Overview
TypeConverterAttribute Class
如何:实现类型转换器

6. 源码

TextBlock at master · DinoChan_Kino.Toolkit.Wpf

转载于:https://www.cnblogs.com/dino623/p/TextBlockHighlightSource.html

<think>我们计划创建一个自定义控件类,用于在Canvas上绘制不规则曲线或线条。根据WPF的绘图机制,我们可以通过继承`FrameworkElement`或使用`Shape`派生类来实现。但考虑到我们只需要绘制线条,且希望直接添加到Canvas中,使用`Path`类结合`PathGeometry`是一个高效且灵活的选择。 然而,用户希望创建一个自定义类,因此我们可以设计一个类,它能够生成一个`Path`对象(该对象可以被添加到Canvas中),或者我们也可以创建一个直接继承自`Shape`的类,这样我们可以利用WPF的绘图系统并支持数据绑定等特性。 但注意:`Shape`类本身是抽象类,其派生类包括`Line`, `Rectangle`, `Ellipse`, `Polygon`, `Polyline`, `Path`等。如果我们希望自定义一个不规则线条,且要求高度自定义,我们可以考虑继承自`Shape`类,然后重写`DefiningGeometry`属性。 但是,根据引用[1]和[2],`Path`控件是功能最强大的一个,可以绘制由直线、圆弧、贝塞尔曲线等组成的图形。因此,我们可以设计一个类,它能够生成一个`Path`对象,或者我们也可以创建一个继承自`Path`的类,并添加一些自定义属性。 然而,用户要求是“可以被添加进入WPF的Canvas控件的类”,所以这个类最终应该是一个`UIElement`。我们有两种选择: 1. 创建一个类,它本身就是一个`UIElement`(例如继承自`Path`),这样可以直接添加到Canvas。 2. 创建一个普通类,它提供一个方法(如`ToPath()`)来生成一个`Path`对象,然后由用户将该`Path`添加到Canvas。 考虑到使用的灵活性,并且参考用户上一次的问题中我们使用了`ToPath()`方法,这次我们可以尝试创建一个继承自`Shape`的类,这样我们可以直接使用它,并且支持WPF的样式、模板、数据绑定等。 设计思路: 类名:`IrregularLine` 继承自 `Shape` 属性: - `Points`:点集合,用于定义线条的各个顶点(类型为`PointCollection`) - 我们还可以添加`IsClosed`属性,用于决定是否闭合(类似于多边形,但这里我们主要做线条,所以默认不闭合) 重写`DefiningGeometry`属性,根据`Points`生成一个`PathGeometry`。 注意:引用[2]中提到,折线(Polyline)是不闭合的,多边形(Polygon)是闭合的。我们的类可以设计成可闭合也可不闭合。 另外,引用[3]中提到了点的顺序,但我们的线条不需要特别指定顺时针或逆时针,因为不填充(只画线条),所以顺序就是连接顺序。 步骤: 1. 创建类,继承自`Shape`。 2. 定义依赖属性`Points`(点集合)和`IsClosed`(是否闭合)。 3. 重写`DefiningGeometry`,根据点集合和是否闭合来创建几何图形。 但是,我们也可以考虑使用`PolyLineSegment`来连接所有点,这样比逐个添加`LineSegment`更高效。 实现: 首先,我们需要定义依赖属性来支持数据绑定和样式设置。 注意:`Shape`类已经提供了`Stroke`、`StrokeThickness`、`Fill`等属性,但我们这里绘制的是线条,所以`Fill`可能没有意义(除非闭合且需要填充)。但为了通用性,我们可以保留,如果闭合且设置了`Fill`,则会填充内部。但用户的需求是线条,所以默认不闭合,且`Fill`为透明。 具体实现如下:</think>### 创建WPF自定义不规则线条类 以下是完整的实现方案,创建一个可直接添加到Canvas控件的不规则线条类,支持曲线绘制和自定义样式: ```csharp using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.Collections.ObjectModel; using System.ComponentModel; /// <summary> /// 自定义不规则线条类,支持添加到Canvas控件 /// </summary> public class IrregularCurve : Shape { #region 依赖属性 /// <summary> /// 控制点集合 /// </summary> public static readonly DependencyProperty PointsProperty = DependencyProperty.Register( "Points", typeof(PointCollection), typeof(IrregularCurve), new FrameworkPropertyMetadata( new PointCollection(), FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure) ); /// <summary> /// 是否平滑曲线 /// </summary> public static readonly DependencyProperty IsSmoothProperty = DependencyProperty.Register( "IsSmooth", typeof(bool), typeof(IrregularCurve), new FrameworkPropertyMetadata( true, FrameworkPropertyMetadataOptions.AffectsRender) ); /// <summary> /// 控制点集合 /// </summary> [TypeConverter(typeof(PointCollectionConverter))] public PointCollection Points { get => (PointCollection)GetValue(PointsProperty); set => SetValue(PointsProperty, value); } /// <summary> /// 是否使用贝塞尔曲线平滑连接 /// </summary> public bool IsSmooth { get => (bool)GetValue(IsSmoothProperty); set => SetValue(IsSmoothProperty, value); } #endregion #region 几何定义 /// <summary> /// 定义线条几何形状 /// </summary> protected override Geometry DefiningGeometry { get { if (Points == null || Points.Count < 2) return Geometry.Empty; var geometry = new PathGeometry(); var figure = new PathFigure { StartPoint = Points[0], IsClosed = false }; if (IsSmooth && Points.Count > 2) { // 使用贝塞尔曲线创建平滑线条 for (int i = 1; i < Points.Count - 1; i += 2) { figure.Segments.Add(new BezierSegment( Points[i], Points[i + 1], Points[i + 1], true)); } } else { // 使用直线段连接点 for (int i = 1; i < Points.Count; i++) { figure.Segments.Add(new LineSegment( Points[i], true)); } } geometry.Figures.Add(figure); return geometry; } } #endregion #region 辅助方法 /// <summary> /// 添加新控制点 /// </summary> public void AddPoint(Point point) { Points.Add(point); InvalidateVisual(); } /// <summary> /// 清除所有点 /// </summary> public void ClearPoints() { Points.Clear(); InvalidateVisual(); } #endregion } ``` ### 在Canvas中使用示例 #### XAML部分 ```xml <Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:YourNamespace" Title="不规则线条示例" Height="450" Width="800"> <Canvas x:Name="DrawingCanvas" Background="WhiteSmoke"> <!-- 添加自定义不规则线条 --> <local:IrregularCurve x:Name="myCurve" Stroke="RoyalBlue" StrokeThickness="3" StrokeDashArray="2 1" IsSmooth="True"/> </Canvas> </Window> ``` #### C#代码部分 ```csharp // 添加控制点创建曲线 myCurve.Points = new PointCollection { new Point(50, 100), new Point(150, 30), new Point(250, 200), new Point(350, 80), new Point(450, 150), new Point(550, 50) }; // 动态添加新点 myCurve.AddPoint(new Point(650, 180)); // 更改线条样式 myCurve.Stroke = new LinearGradientBrush( Colors.DarkViolet, Colors.Cyan, 45); myCurve.StrokeThickness = 4; myCurve.StrokeDashArray = new DoubleCollection { 4, 2 }; // 切换平滑模式 myCurve.IsSmooth = false; // 切换为折线模式 ``` ### 关键功能说明 1. **几何定义** 通过重写`DefiningGeometry`属性,使用`PathGeometry`动态生成线条几何形状: - 当`IsSmooth=true`时使用`BezierSegment`创建平滑曲线 - 当`IsSmooth=false`时使用`LineSegment`创建折线[^1] 2. **依赖属性** 使用DependencyProperty实现高效数据绑定和UI更新: ```csharp public static readonly DependencyProperty PointsProperty = ... ``` 3. **动态更新** 调用`InvalidateVisual()`强制重绘: ```csharp Points.Add(point); InvalidateVisual(); // 触发重绘 ``` 4. **样式自定义** 继承自`Shape`类,支持所有标准样式属性: ```xaml <local:IrregularCurve Stroke="Red" StrokeThickness="3" StrokeDashArray="2 1"/> ``` 5. **平滑控制** 通过`IsSmooth`属性切换曲线/折线模式: ```csharp myCurve.IsSmooth = false; // 切换为折线 ``` ### 性能优化建议 1. **使用StreamGeometry** 对于高频更新的场景,替换为`StreamGeometry`提升性能[^1]: ```csharp var geometry = new StreamGeometry(); using (var context = geometry.Open()) { context.BeginFigure(Points[0], false, false); // 添加线段... } return geometry; ``` 2. **数据绑定支持** 实现`INotifyPropertyChanged`支持MVVM模式: ```csharp public class IrregularCurve : Shape, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; } ``` 3. **命中测试优化** 重写`HitTestCore`提高交互性能: ```csharp protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParams) { // 自定义命中测试逻辑 } ``` ### 应用场景示例 ```csharp // 创建心电图效果 var ecgCurve = new IrregularCurve { Stroke = Brushes.Red, StrokeThickness = 2, IsSmooth = true }; // 添加动态数据点 for (int i = 0; i < 100; i++) { ecgCurve.AddPoint(new Point(i * 5, 100 + 50 * Math.Sin(i * 0.2))); } // 添加到Canvas DrawingCanvas.Children.Add(ecgCurve); Canvas.SetZIndex(ecgCurve, 10); // 设置叠放层级 ``` 此实现提供了灵活的不规则线条绘制功能,支持平滑曲线和折线模式,可直接添加到Canvas控件,并可通过样式属性完全自定义外观[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值