Drawing multicolor string with wrapping in WinForms app
In the last post I said, that drawing multicolor string with wrapping in WinForms application is not an easy task if you don't want to use GDI+
engine. But let's imagine, that your boss came to you and said, that you should do such a feature until tomorrow of he will cut your salary twice. Is there any chance to survive instead of running around of office and screaming? Yes, there is one trick. We will use the power of WPF in our WinForms
app.
Yep, I wrote it right: we can use WPF
controls in WinForms
app and vica versa. The main door to the WPF
world from WinForms
is ElementHost control. I'll illustrate this on a simple example: highlighting all occurrences of a given substring in a text. First things first, do the following:
- Create a new WinForms project.
- Add
ElementHost
control to the form. - Add 2
TextBoxes
, one will be calledmainTextBox
(where reference text will be placed), second -findTextBox
(here we will place text to highlight). - Add
CheckBox
, it will indicate whenever text direction should be right to left or not (there is not only English language in the world). - Add a new
WPF
UserControl
to the project with the followingXAML
markup:
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestWinForms"
x:Class="TestWinForms.MyUserControl"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TextBlock TextWrapping="Wrap" Text="Hello world!"/>
</UserControl>
The next thing we should do is to setup ViewModel
since WPF
follows MVVM pattern:
namespace TestWinForms
{
public class ViewModel : INotifyPropertyChanged
{
private string mainText = string.Empty;
public string MainText
{
get { return mainText; }
set
{
mainText = value;
OnNotifyPropertyChanged("MainText");
}
}
private string findText = string.Empty;
public string FindText
{
get { return findText; }
set
{
findText = value;
OnNotifyPropertyChanged("FindText");
}
}
private FlowDirection flowDirection = FlowDirection.LeftToRight;
public FlowDirection FlowDirection
{
get { return flowDirection; }
set
{
flowDirection = value;
OnNotifyPropertyChanged("FlowDirection");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnNotifyPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Now it's time to bind WPF
and WinForms
; we should create one handler for Form.Load, 2 handlers for TextBox.TextChanged events and 1 handler for CheckBox.CheckedChanged:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TestWinForms
{
public partial class MyForm : Form
{
private ViewModel viewModel;
public MyForm()
{
InitializeComponent();
}
private void MyForm_Load(object sender, EventArgs e)
{
var wpfControl = new MyUserControl();
viewModel = new ViewModel();
elementHost.Child = wpfControl;
wpfControl.DataContext = viewModel;
}
private void mainTextBox_TextChanged(object sender, EventArgs e)
{
viewModel.MainText = mainTextBox.Text;
}
private void findTextBox_TextChanged(object sender, EventArgs e)
{
viewModel.FindText = findTextBox.Text;
}
private void directionCb_CheckedChanged(object sender, EventArgs e)
{
if (directionCb.Checked)
{
viewModel.FlowDirection = System.Windows.FlowDirection.RightToLeft;
}
else
{
viewModel.FlowDirection = System.Windows.FlowDirection.LeftToRight;
}
}
}
}
Everything is pretty straightforward. On the Load
event we initialize WPF
control and attach it to the ElementHost
. Also we create the instance of our ViewModel
and assign it to the DataContext property. TextChanged
events update corresponding properties in a ViewModel
. And a little change at the WPF
control markup:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestWinForms"
x:Class="TestWinForms.MyUserControl"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TextBlock TextWrapping="Wrap" Text="{Binding MainText}">
</TextBlock>
</UserControl>
Build and run the app. Now when you type text at the mainText
TextBox
it should be shown at the WPF
control. But it comes the question: How to display formatted text? Ok, let's take a look on the TextBlock class description. What we need is Inlines property, but it is not a dependency property (which only can be the target of binding). This issue may be solved by creating Attached Property:
using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace TestWinForms
{
public static class TextBlockEx
{
public static Inline GetFormattedText(DependencyObject obj)
{
return (Inline)obj.GetValue(FormattedTextProperty);
}
public static void SetFormattedText(DependencyObject obj, Inline value)
{
obj.SetValue(FormattedTextProperty, value);
}
public static readonly DependencyProperty FormattedTextProperty =
DependencyProperty.RegisterAttached(
"FormattedText",
typeof(Inline),
typeof(TextBlockEx),
new PropertyMetadata(null, OnFormattedTextChanged));
private static void OnFormattedTextChanged(
DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
var textBlock = o as TextBlock;
if (textBlock == null) return;
var inline = (Inline)e.NewValue;
textBlock.Inlines.Clear();
if (inline == null) return;
textBlock.Inlines.Add(inline);
}
}
}
Using it we can update TextBlock.Inlines
collection. But we also need to create Inline
object somehow, keeping in mind that we have 2 strings as input: string with main text and string with text to find. Converters to the rescue. Here comes implementation of IMultiValueConverter interface:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
namespace TestWinForms
{
public class TextToInlineConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var mainText = (string)values[0];
var findText = (string)values[1];
if (string.IsNullOrEmpty(findText))
{
return CreateSimpleInline(mainText);
}
var index = mainText.IndexOf(findText, StringComparison.OrdinalIgnoreCase);
var regions = new List<Region>();
while (index != -1)
{
var match = new Region(index, findText.Length);
regions.Add(match);
index = mainText.IndexOf(findText, index + findText.Length, StringComparison.OrdinalIgnoreCase);
}
var span = new Span();
if (regions.Count == 0)
{
return CreateSimpleInline(mainText);
}
else
{
var position = 0;
for (int i = 0; i < regions.Count; i++)
{
span.Inlines.Add(mainText.Substring(position, regions[i].StartIndex - position));
position += regions[i].StartIndex - position;
span.Inlines.Add(new Run(mainText.Substring(regions[i].StartIndex, regions[i].Length))
{
Foreground = Brushes.Red,
Background = Brushes.Yellow
});
position += regions[i].Length;
if (i == regions.Count - 1)
{
span.Inlines.Add(new Run(mainText.Substring(position)));
}
}
}
return span;
}
private Inline CreateSimpleInline(string text)
{
return new Run(text);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
private struct Region
{
public int StartIndex { get; set; }
public int Length { get; set; }
public Region(int startIndex, int length)
: this()
{
StartIndex = startIndex;
Length = length;
}
}
}
}
Wiring up these pieces together in XAML
markup:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestWinForms"
x:Class="TestWinForms.MyUserControl"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TextBlock TextWrapping="Wrap" FlowDirection="{Binding FlowDirection}">
<TextBlock.Resources>
<local:TextToInlineConverter x:Key="HighlightConverter"/>
</TextBlock.Resources>
<local:TextBlockEx.FormattedText>
<MultiBinding Converter="{StaticResource ResourceKey=HighlightConverter}">
<Binding Path="MainText"/>
<Binding Path="FindText"/>
</MultiBinding>
</local:TextBlockEx.FormattedText>
</TextBlock>
</UserControl>
We've got the desired behavior, congratulations:
By the way, using this approach you can easily create components with complex UI in WinForms
apps if you don't have enough time or resources to do them using WinForms
, I don't think it will blow away your karma unless you are old-fashioned pirate programmer, who prefers only "pure" apps.