This article is currently in the process of being translated into Polish (~99% done).
How-to: Creating a Rich Text Editor
To jest kolejny poradnik na tej stronie, który został zainspirowany przez kontrolkę RichTextBox. Jej łatwość łatwość w użyciu pozwala nam na stworzenie małego, lecz w pełni funkcjonalnego edytora tekstu (takiego jak Wordpad!). Mimo, że WPF znacznie ułatwia nam pracę, tym razem będziemy musieli napisać nieco więcej kodu niż zwykle, ale nie przejmuj się tym. Opiszemy najpierw najciekawsze sekcje po kolei, a pod koniec pokażę Ci cały kod.
W tym artykule będziemy używać dużo technik i kontrolek opisanych wcześniej, więc wyjaśnienia poszczególnych elementów nie będą bardzo szczegółowe. Pamiętaj, że zawsze możesz cofnąć się do poprzednich artykułów, jeśli potrzebujesz odświeżyć swoją wiedzę!
Na początek zobaczmy, co w ogóle będziemy tworzyć. Tak powinien wyglądać efekt końcowy:
Interfejs
Interfejs naszej aplikacji zawiera pasek narzędzi z przyciskami i listami wyboru. Przewidziane są: przyciski do otwierania i zapisywania dokumentów, przyciski do formatowania tekstu oraz dwie listy wyboru służące do wyboru czcionki i rozmiaru tekstu.
Poniżej paska narzędzi znajduje się kontrolka RichTextBox, która będzie obsługiwała całą resztę.
Komendy
Pierwszą rzeczą, którą mogłeś zauważyć jest użycie komend WPF, które omawialiśmy we wcześniejszym artykule. Skorzystamy z komend Open i Save znajdujących się w klasie ApplicationCommands, aby otwierać i zapisywać dokumenty. Aby formatować tekst (pogrubienie, kursywa, podkreślenie), użyjemy komend ToggleBold, ToggleItalic i ToggleUnderline z klasy EditingCommands.
Korzyść z używania komend do formatowania (ToggleBold, ToggleItalic, ...) jest taka, że RichTextBox domyślnie je implementuje. Oznacza to, że nie musimy pisać żadnego kodu - wystarczy podpiąć odpowiednie komendy do odpowiednich przycisków i wszystko działa jak należy:
<ToggleButton Command="EditingCommands.ToggleBold" Name="btnBold">
<Image Source="/WpfTutorialSamples;component/Images/text_bold.png" Width="16" Height="16" />
</ToggleButton>
Wraz z użyciem komend otrzymujemy również znane skróty klawiszowe bez żadnego dodatkowego wysiłku (Ctrl + B dla pogrubienia, Ctrl + I dla kursywy i Ctrl + U dla podkreślenia)!
Warto zauważyć, że użyłem kontrolki ToggleButton zamiast zwykłego Button. Najważniejsza różnica pomiędzy tymi kontrolkami jest taka, że ToggleButton w przeciwieństwie do zwykłego przycisku, ma dwa stany - wciśnięty i nie. Dzięki temu, gdy zaznaczony tekst jest pogrubiony, przełącznik będzie włączony i vice versa. Stan ToggleButton możemy zmieniać za pomocą właściwości IsChecked. Niestety WPF nie robi tego automatycznie, więc tę część aplikacji będziemy musieli zaprogramować sami. Więcej dowiemy się później.
Komendy zapisywania i otwierania nie mogą być obsłużone automatycznie, więc będziemy musieli je zaimplementować, jak zwykle, za pomocą CommandBinding okna i zdarzenia w Code-behind.
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open" Executed="Open_Executed" />
<CommandBinding Command="ApplicationCommands.Save" Executed="Save_Executed" />
</Window.CommandBindings>
Pokażę Ci implementację później,
Krój i rozmiar czcionki
Aby pokazywać i zmieniać krój i rozmiar tekstu mamy kilka list wybieranych. Są one uzupełnione czcionkami systemowymi i listą możliwych rozmiarów tekstu, która jest zawarta w konstruktorze okna:
public RichTextEditorSample()
{
InitializeComponent();
cmbFontFamily.ItemsSource = Fonts.SystemFontFamilies.OrderBy(f => f.Source);
cmbFontSize.ItemsSource = new List<double>() { 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 };
}
Znowu, WPF ułatwia nam bardzo zdobycie listy wszystkich dostępnych czcionek poprzez użycie właściwości SystemFontFamilies. Ponieważ lista dostępnych rozmiarów tekstu jest tylko sugestią, więc damy użytkownikowi możliwość wpisania własnej wielkości:
<ComboBox Name="cmbFontFamily" Width="150" SelectionChanged="cmbFontFamily_SelectionChanged" />
<ComboBox Name="cmbFontSize" Width="50" IsEditable="True" TextBoxBase.TextChanged="cmbFontSize_TextChanged" />
To oznacza, że będziemy inaczej obsługiwać zmiany wartości ComboBoxów. Dla listy czcionek wystarczy zdarzenie SelectionChanged, lecz dla listy rozmiarów użyjemy TextBoxBase.TextChanged, bo użytkownik może wybrać predefiniowany rozmiar lub wpisać swój własny.
WPF obsługuje automatycznie komendy pogrubienia, kursywy i podkreślenia, ale rozmiar i krój tekstu musimy zaimplementować sami. Na szczęście jest na to łatwy sposób - użycie metody ApplyPropertyValue(). Wyżej wymienione metody obsługi zdarzeń dla list wyglądają więc tak:
private void cmbFontFamily_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(cmbFontFamily.SelectedItem != null)
rtbEditor.Selection.ApplyPropertyValue(Inline.FontFamilyProperty, cmbFontFamily.SelectedItem);
}
private void cmbFontSize_TextChanged(object sender, TextChangedEventArgs e)
{
rtbEditor.Selection.ApplyPropertyValue(Inline.FontSizeProperty, cmbFontSize.Text);
}
Nie ma tutaj nic specjalnego - po prostu przekazujemy wybrane lub wpisane wartości do metody ApplyPropertyValue() razem z właściwością, którą chcemy zmienić.
Aktualizowanie interfejsu
Jak wcześniej wspomnieliśmy, WPF obsługuje komendy pogrubienia, kursywy i podkreślenia, ale musimy sami zająć się stanem ich przycisków, ponieważ komendy nie mają takiej funkcjonalności. Nie przejmuj się tym zbytnio, bo i tak musimy zaktualizować stan ComboBoxów kroju i rozmiaru tekstu.
Chcemy zaktualizować te wartości przy zmianie pozycji kursora lub przy zaznaczeniu jakiegokolwiek tekstu. Zdarzenie SelectionChanged idealnie do tego zastosowania pasuje. Tak będzie wyglądała nasza metoda:
private void rtbEditor_SelectionChanged(object sender, RoutedEventArgs e)
{
object temp = rtbEditor.Selection.GetPropertyValue(Inline.FontWeightProperty);
btnBold.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(FontWeights.Bold));
temp = rtbEditor.Selection.GetPropertyValue(Inline.FontStyleProperty);
btnItalic.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(FontStyles.Italic));
temp = rtbEditor.Selection.GetPropertyValue(Inline.TextDecorationsProperty);
btnUnderline.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(TextDecorations.Underline));
temp = rtbEditor.Selection.GetPropertyValue(Inline.FontFamilyProperty);
cmbFontFamily.SelectedItem = temp;
temp = rtbEditor.Selection.GetPropertyValue(Inline.FontSizeProperty);
cmbFontSize.Text = temp.ToString();
}
Na pierwszy rzut oka wydaje się, że jest tutaj bardzo dużo kodu, ale w rzeczywistości zmiana stanu kontrolki zajmuje tylko dwie linijki. Więc skąd wzięło się tutaj tyle instrukcji? Do obsłużenia mamy aż 3 przyciski i 2 listy, więc ten sam kod powtarzamy kilka razy z drobnymi zmianami.
Zasada działania jest tutaj niezwykle prosta. Dla przycisków używamy metody GetPropertyValue(), aby otrzymać bieżącą właściwość zaznaczonego tekstu (np. pogrubienie). Potem po prostu zmieniamy wartość IsChecked odpowiedniego przycisku na podstawie otrzymanej właściwości.
Dla list wyboru robimy to samo, ale zamiast ustawiania właściwości IsChecked, zmieniamy zaznaczony element lub tekst ComboBoxa zgodnie z otrzymanymi wartościami.
Otwieranie i zapisywanie pliku
Przy obsługiwaniu komend Open i Save, użyjemy podobnego kodu:
private void Open_Executed(object sender, ExecutedRoutedEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|All files (*.*)|*.*";
if(dlg.ShowDialog() == true)
{
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Open);
TextRange range = new TextRange(rtbEditor.Document.ContentStart, rtbEditor.Document.ContentEnd);
range.Load(fileStream, DataFormats.Rtf);
}
}
private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|All files (*.*)|*.*";
if(dlg.ShowDialog() == true)
{
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Create);
TextRange range = new TextRange(rtbEditor.Document.ContentStart, rtbEditor.Document.ContentEnd);
range.Save(fileStream, DataFormats.Rtf);
}
}
Dialogi OpenFileDialog i SaveFileDialog są użyte do sprecyzowania lokalizacji i nazwy naszego pliku, a następnie tekst jest wczytany lub zapisany używając obiektu TextRange, który uzyskujemy bezpośrednio z kontrolki RichTextBox, z połączeniem obiektu FileStream, który zapewnia dostęp do fizycznego pliku. Dokument ten jest później otworzony lub zapisany w formacie RTF, ale jeśli chcesz aby Twój edytor wspierał inne rozszerzenia plików, możesz zawsze sprecyzować inny format, np. czysty tekst (.txt).
Całkowity przykład
Poniżej znajduje się kod dla całej aplikacji - najpierw interfejs zapisany w XAML, a potem Code-behind w C#:
<Window x:Class="WpfTutorialSamples.Rich_text_controls.RichTextEditorSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="RichTextEditorSample" Height="300" Width="400">
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open" Executed="Open_Executed" />
<CommandBinding Command="ApplicationCommands.Save" Executed="Save_Executed" />
</Window.CommandBindings>
<DockPanel>
<ToolBar DockPanel.Dock="Top">
<Button Command="ApplicationCommands.Open">
<Image Source="/WpfTutorialSamples;component/Images/folder.png" Width="16" Height="16" />
</Button>
<Button Command="ApplicationCommands.Save">
<Image Source="/WpfTutorialSamples;component/Images/disk.png" Width="16" Height="16" />
</Button>
<Separator />
<ToggleButton Command="EditingCommands.ToggleBold" Name="btnBold">
<Image Source="/WpfTutorialSamples;component/Images/text_bold.png" Width="16" Height="16" />
</ToggleButton>
<ToggleButton Command="EditingCommands.ToggleItalic" Name="btnItalic">
<Image Source="/WpfTutorialSamples;component/Images/text_italic.png" Width="16" Height="16" />
</ToggleButton>
<ToggleButton Command="EditingCommands.ToggleUnderline" Name="btnUnderline">
<Image Source="/WpfTutorialSamples;component/Images/text_underline.png" Width="16" Height="16" />
</ToggleButton>
<Separator />
<ComboBox Name="cmbFontFamily" Width="150" SelectionChanged="cmbFontFamily_SelectionChanged" />
<ComboBox Name="cmbFontSize" Width="50" IsEditable="True" TextBoxBase.TextChanged="cmbFontSize_TextChanged" />
</ToolBar>
<RichTextBox Name="rtbEditor" SelectionChanged="rtbEditor_SelectionChanged" />
</DockPanel>
</Window>
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Win32;
using System.Windows.Controls;
namespace WpfTutorialSamples.Rich_text_controls
{
public partial class RichTextEditorSample : Window
{
public RichTextEditorSample()
{
InitializeComponent();
cmbFontFamily.ItemsSource = Fonts.SystemFontFamilies.OrderBy(f => f.Source);
cmbFontSize.ItemsSource = new List<double>() { 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 };
}
private void rtbEditor_SelectionChanged(object sender, RoutedEventArgs e)
{
object temp = rtbEditor.Selection.GetPropertyValue(Inline.FontWeightProperty);
btnBold.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(FontWeights.Bold));
temp = rtbEditor.Selection.GetPropertyValue(Inline.FontStyleProperty);
btnItalic.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(FontStyles.Italic));
temp = rtbEditor.Selection.GetPropertyValue(Inline.TextDecorationsProperty);
btnUnderline.IsChecked = (temp != DependencyProperty.UnsetValue) && (temp.Equals(TextDecorations.Underline));
temp = rtbEditor.Selection.GetPropertyValue(Inline.FontFamilyProperty);
cmbFontFamily.SelectedItem = temp;
temp = rtbEditor.Selection.GetPropertyValue(Inline.FontSizeProperty);
cmbFontSize.Text = temp.ToString();
}
private void Open_Executed(object sender, ExecutedRoutedEventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|All files (*.*)|*.*";
if(dlg.ShowDialog() == true)
{
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Open);
TextRange range = new TextRange(rtbEditor.Document.ContentStart, rtbEditor.Document.ContentEnd);
range.Load(fileStream, DataFormats.Rtf);
}
}
private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "Rich Text Format (*.rtf)|*.rtf|All files (*.*)|*.*";
if(dlg.ShowDialog() == true)
{
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Create);
TextRange range = new TextRange(rtbEditor.Document.ContentStart, rtbEditor.Document.ContentEnd);
range.Save(fileStream, DataFormats.Rtf);
}
}
private void cmbFontFamily_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(cmbFontFamily.SelectedItem != null)
rtbEditor.Selection.ApplyPropertyValue(Inline.FontFamilyProperty, cmbFontFamily.SelectedItem);
}
private void cmbFontSize_TextChanged(object sender, TextChangedEventArgs e)
{
rtbEditor.Selection.ApplyPropertyValue(Inline.FontSizeProperty, cmbFontSize.Text);
}
}
}
Tutaj znajduje się kolejny screenshot, w którym zaznaczyliśmy trochę tekstu. Zauważ, że kontrolki na pasku narzędzi dostosowały się do zaznaczonego fragmentu:
Podsumowanie
Jak widzisz, stworzenie edytora tekstu w WPF jest bardzo proste, głównie dzięki świetnej kontrolce RichTextBox. Jeśli chcesz, możesz łatwo rozszerzyć ten przykład do rzeczy takich jak wyrównywanie tekstu, kolory, listy punktowane a nawet tabelki!
Bądź świadomy, że o ile ten przykład działa prawidłowo, nie ma w nim żadnej obsługi błędów lub sprawdzania, czy wpisane wartości są prawidłowe. Jest wiele miejsc, w których nasza aplikacja może się zawiesić, np. ComboBox rozmiaru tekstu. Gdy wpiszemy tam nienumeryczne wartości, wystąpi crash. Jeśli więc chcesz poszerzyć ten przykład, powinieneś zaimplementować walidację danych i obsługę błędów.