PreCode Wyświetlanie kodu na bloga. Tak mam na ten temat wiele wpisów. Miesiąc temu postanowiłem stworzyć narzędzie w WPF, które pomoże mi jeszcze bardziej przyspieszyć proces dodawania kodu do wpisów oraz do slajdów na webinary.

Taka aplikacja już istniała i nazywała się "PreCoder". Domyślnie był to plugin do Windows Live Writer. Tak kiedyś tak pisałem wpisy, a teraz to nawet Windows Live Writer nie będzie chciał Ci się zainstalować, ponieważ Microsoft zablokował ci taką możliwość. Odpowiedni Open Source tego programu z tego, co wiem to nie ma takiego pluginu. Jego ostatnia aktualizacja była w roku 2017. 

Sam "PreCoder" można uruchomić jako normalną aplikację WPF. Niestety nie spełniała ona wszystkim moich potrzeb więc potrzebowałem czegoś podobnego, ale rozbudowanego. Będę jednak szczery, że ułatwiłem sobie zdanie i logikę formatowania kodu częściowo skopiowałem właśnie z tego programu. 

W tym wpisie pokaże ci jak utworzyć szybko pomocniczą aplikację w WPF która :

  • Usunie białe znaki 
  • Wyrówna odstępy w kodzie
  • Zrobi kodowanie HTML (HTML Encode) na tekscie
  • Tekst automatycznie zostanie skopiowany do schowka
  • Otoczy tekst tagami <pre> i <code> jeśli będziemy tego chcieli
  • Doda odpowiednie paragrafy <p> do tekstu, jeśli będziemy tego chcieli
  • Otworzy rezultat naszego działania w notatniku, jeśli będziemy tego chcieli
  • Doda odpowiednią klasę CSS zależności od typu kodu. Jest to ważne dla biblioteki JavaScript Highlight.js, która koloryzuje składnie na tym blogu
  • Doda atrybut, który określi tytuł kodu. Coś co kiedyś stworzyłem na swoim blogu, ale dawno z tego nie korzystałem, ponieważ nie chciało mi się dodawać atrybutu data-codetitle ręcznie.

Aplikacja ma działać tak.

messagebox.gif

Przejdźmy więc do pracy. Nie będzie żadnych wzorców projektowych MVVM w tym projekcie chce po prostu mieć aplikację, która działa.

Kod można pobrać tutaj : GitHub - PanNiebieski/PreCodeTextFormaterWPF

Jak otworzyć notatnik i dopisać do niego tekst w C#?

Jak otworzyć notatnik w C#?

To proste wystarczy odpalić proces "notepad.exe". Jednak aby skorzystać z czegoś więcej trzeba się bardziej wysilić.

Trzeba skorzystać, z których bibliotek nie ma .NET.  Importuje więc bibliotekę "user32.dll". W niej są metody, które pozwolą ustawić na przykład ustawić rozmiar oraz miejsce okna notatnika. Jest to metoda FindWindowEx.

Mamy tutaj także metodę "SetWindowText", która ustawia tekst okna.

Mamy także tutaj metodę "SendMessage", która pozwoli mi wysłać tekst do otwartego okna notatnika.

public static class NotepadHelper
{
    [DllImport("user32.dll", EntryPoint = "SetWindowText")]
    private static extern int SetWindowText(IntPtr hWnd, string text);

    [DllImport("user32.dll", EntryPoint = "FindWindowEx")]
    private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

    [DllImport("User32.dll", EntryPoint = "SendMessage")]
    private static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);

    public static void ShowMessage(string message = null, string title = null)
    {
        Process notepad = Process.Start(new ProcessStartInfo("notepad.exe"));
        if (notepad != null)
        {
            notepad.WaitForInputIdle();

            if (!string.IsNullOrEmpty(title))
                SetWindowText(notepad.MainWindowHandle, title);

            if (!string.IsNullOrEmpty(message))
            {
                IntPtr child = FindWindowEx(notepad.MainWindowHandle, new IntPtr(0), "Edit", null);
                SendMessage(child, 0x000C, 0, message);
            }
        }
    }
}

Oto klasa pomocnicza, która otworzy nam okno notatnika już odpowiednią treścią i nazwą okna.

Początkowy widok XAML

Każda aplikacja WPF potrzebuje UI. Oto czym interfejs jest otoczony.

Grid w Border, który ma Border, który ma Grida.

<Window x:Class="PreCodeTextFormater.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:local="clr-namespace:PreCodeTextFormater"
        mc:Ignorable="d" Title="PreCode Code Window"
        WindowStartupLocation="CenterScreen"
        ShowInTaskbar="True"
        Background="Transparent"
        MinHeight="550"
        MinWidth="800"
         ResizeMode="CanResizeWithGrip"
WindowStyle="ToolWindow"
         Icon="Icon.ico"
    Margin="0"
    Padding="0"
        Height="550" Width="800" >
    <Grid>
        <Border CornerRadius="5" Background="Black" BorderBrush="#777" BorderThickness="1" Margin="0" Padding="0">
            <Border CornerRadius="5" Background="Black" BorderBrush="#000" BorderThickness="1" Margin="0" Padding="0">
                <Grid Margin="0">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <!--Toolbar TUTAJ -->
                    <!--Text Area TUTAJ -->
                </Grid>
            </Border>
        </Border>
    </Grid>
</Window>

A oto definicja paska narzędzi w naszej aplikacji. 

<!--Toolbar -->
<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="1,0,-2,0" BorderBrush="#666" CornerRadius="1" BorderThickness="1">
    <Border.Background>
        <RadialGradientBrush Center="0.5,0.2" GradientOrigin="0.5,-0.2" RadiusX="0.5" RadiusY="0.5">
            <GradientStop Color="black"  Offset="0"/>
            <GradientStop Color="#55111111" Offset="1"/>
        </RadialGradientBrush>
    </Border.Background>
    <StatusBar x:Name="MainToolBar" Padding="0" Margin="0" BorderBrush="#555" BorderThickness="0">
        <StatusBar.Background>
            <LinearGradientBrush StartPoint="0.486,0" EndPoint="0.486,0.986">
                <GradientStop Color="#33666666" Offset="0"/>
                <GradientStop Color="#33666666" Offset="0.4"/>
                <GradientStop Color="#BB222222" Offset="0.5"/>
                <GradientStop Color="#BB111111" Offset="1"/>
            </LinearGradientBrush>
        </StatusBar.Background>
        <StatusBarItem Margin="1,1" Padding="0">
            <Button Background="Gray" Foreground="White" x:Name="Button_DeDent" Padding="12,12,12,12"  Click="Button_DeDent_Click">&lt;&lt;</Button>
        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <Button Background="Gray" Foreground="White" x:Name="Button_InDent" Padding="12,12,12,12"  Click="Button_InDent_Click">&gt;&gt;</Button>
        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <Button Background="Gray" Foreground="White" x:Name="Button_FixIndentation" Padding="12,12,12,12"  Click="Button_FixIndentation_Click">Fix Indentation</Button>
        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <StackPanel Orientation="Horizontal">
                <Line Margin="4,0,0,0" SnapsToDevicePixels="False" Stroke="#55000000" Stretch="Fill" X1="0" Y1="0" X2="0" Y2="16" />
                <Line Margin="0,0,4,0" SnapsToDevicePixels="False" Stroke="#FF999999" Stretch="Fill" X1="0.5" Y1="0" X2="0.5" Y2="16" />
            </StackPanel>
        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <StackPanel Orientation="Vertical">
                <StackPanel Orientation="Horizontal">
                    <CheckBox IsChecked="True" x:Name="CheckBox_HtmlEncode" Margin="0,7,0,7" HorizontalAlignment="Left" Foreground="Gray">Html Encode</CheckBox>
                    <CheckBox IsChecked="True" x:Name="CheckBox_AddPreCode" Margin="7,7,0,7" HorizontalAlignment="Left" Foreground="Gray">Add Pre/Code</CheckBox>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <CheckBox IsChecked="True" x:Name="CheckBox_OpenNotepad" Margin="0,7,0,7" HorizontalAlignment="Left" Foreground="Gray">OpenNotpad</CheckBox>
                    <CheckBox IsChecked="True" x:Name="CheckBox_AddParagraf" Margin="7,7,0,7" HorizontalAlignment="Left" Foreground="Gray">Add &lt;p&gt;</CheckBox>
                    <CheckBox x:Name="CheckBox_LineEndings" Margin="0,7,0,7" Visibility="Collapsed" HorizontalAlignment="Left" Foreground="Gray">Swap CRLFs with &lt;BR&gt;</CheckBox>
                </StackPanel>
            </StackPanel>

        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <StackPanel Orientation="Horizontal">
                <ComboBox    x:Name="ComboBox_Language" Width="90" Margin="7,3,0,4" Foreground="Black" HorizontalAlignment="Center"  SelectionChanged="ComboBox_SurroundWith_SelectionChanged">
                    <ComboBoxItem Name="None" Tag="" Content="None"></ComboBoxItem>
                    <ComboBoxItem Name="CSharp" Tag="cs" Content="CSharp"  IsSelected="True"></ComboBoxItem>
                    <ComboBoxItem Name="SQL" Tag="sql" Content="SQL"></ComboBoxItem>
                    <ComboBoxItem Name="HTML" Tag="html"  Content="HTML"></ComboBoxItem>
                    <ComboBoxItem Name="JavaScript" Tag="js" Content="JavaScript"></ComboBoxItem>
                    <ComboBoxItem Name="CSS" Tag="css"  Content="CSS"></ComboBoxItem>
                    <ComboBoxItem Name="Java" Tag="java"  Content="Java"></ComboBoxItem>
                    <ComboBoxItem Name="Angular" Tag="angular js"  Content="Angular"></ComboBoxItem>
                    <ComboBoxItem Name="Typescript" Tag="typescript js" Content="Typescript"></ComboBoxItem>
                    <ComboBoxItem Name="Xaml" Tag="xaml xml" Content="Xaml"></ComboBoxItem>
                    <ComboBoxItem Name="Xml" Tag="xml" Content="Xml"></ComboBoxItem>
                    <ComboBoxItem Name="objectivec" Tag="objectivec"  Content="Objectivec"></ComboBoxItem>
                    <ComboBoxItem Name="Razor" Tag="razor"  Content="Razor"></ComboBoxItem>
                    <ComboBoxItem Name="Json"  Tag="json"  Content="JSON"></ComboBoxItem>
                    <ComboBoxItem Name="T4"   Tag="t4 cs" Content="T4"></ComboBoxItem>
                    <ComboBoxItem Name="NPM" Tag="npm" Content="NPM"></ComboBoxItem>
                    <ComboBoxItem Name="Fsharp" Tag="fs" Content="Fsharp"></ComboBoxItem>
                    <ComboBoxItem Name="CiJava" Tag="csjava"  Content="C# i Java"></ComboBoxItem>
                    <ComboBoxItem Name="Powershell" Tag="powershell" Content="Powershell"></ComboBoxItem>
                    <ComboBoxItem Name="Http" Tag="http" Content="Http"></ComboBoxItem>
                    <ComboBoxItem Name="COBOL" Tag="cobol" Content="COBOL"></ComboBoxItem>
                    <ComboBoxItem Name="COMMANDLINEINTERFACE" Tag="cmd" Content="CMD"></ComboBoxItem>
                    <ComboBoxItem Name="CPlusPlus" Tag="cpp" Content="CPlusPlus"></ComboBoxItem>
                    <ComboBoxItem Name="Python" Tag="python" Content="Python"></ComboBoxItem>
                    <ComboBoxItem Name="LessCSS" Tag="less" Content="Less CSS"></ComboBoxItem>
                    <ComboBoxItem Name="Scss" Tag="scss" Content="Scsc"></ComboBoxItem>
                </ComboBox>
                <TextBox  Foreground="Black" Margin="7,3,0,4" Width="100"  x:Name="TXT_CodeTitle"   Text="{Binding CodeTitle, Mode=TwoWay}"  />
            </StackPanel>
        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <StackPanel Orientation="Horizontal">
                <Line Margin="4,0,0,0" SnapsToDevicePixels="False" Stroke="#55000000" Stretch="Fill" X1="0" Y1="0" X2="0" Y2="16" />
                <Line Margin="0,0,4,0" SnapsToDevicePixels="False" Stroke="#FF999999" Stretch="Fill" X1="0.5" Y1="0" X2="0.5" Y2="16" />
            </StackPanel>
        </StatusBarItem>
        <StatusBarItem Margin="1,1" Padding="0">
            <Button BorderBrush="#C6FA46" Padding="12,12,12,12" Background="Black" Margin="0,0,0,0" Foreground="#C6FA46" x:Name="Button_Ok"  Click="Button_Ok_Click">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Get Code to ClipArt" Padding="0,0,7,0" />
                    <Image Source="icon48.png" Height="16" Width="16"  />
                </StackPanel>
            </Button>
        </StatusBarItem>
    </StatusBar>
</Border>

Skupmy się na razie przyciskach, którym celem jest dodawanie lub odejmowanie akapitu.

<StatusBarItem Margin="1,1" Padding="0">
    <Button Background="Gray" Foreground="White" x:Name="Button_DeDent" Padding="12,12,12,12"  Click="Button_DeDent_Click">&lt;&lt;</Button>
</StatusBarItem>
<StatusBarItem Margin="1,1" Padding="0">
    <Button Background="Gray" Foreground="White" x:Name="Button_InDent" Padding="12,12,12,12"  Click="Button_InDent_Click">&gt;&gt;</Button>
</StatusBarItem>
<StatusBarItem Margin="1,1" Padding="0">
    <Button Background="Gray" Foreground="White" x:Name="Button_FixIndentation" Padding="12,12,12,12"  Click="Button_FixIndentation_Click">Fix Indentation</Button>
</StatusBarItem>

Zanim zajrzymy do zdarzeń Click w tych przyciskach. Warto omówić wiele metod pomocniczych

Po pierwsze przyda się metoda, która zamieni spacje na tabulację. Przy czym warto ustalić stałą liczbową jak wielka ta tabulacja powinna być. 

private void SwapTabsForSpaces()
{
    // swap any tab whitespaces for spaces since amount will always be in spaces.
    TextBox_Code.Text = TextBox_Code.Text.Replace("\t", " ".PadLeft(TAB_SIZE));
}

Kolejna metoda pomocnicza obliczy zasięg zaznaczonego przez Ciebie tekstu w polu tekstowym.

private void GetLineRange(string[] lines, ref int first, ref int last)
{
    try
    {
        bool foundFirst = false;
        bool foundLast = false;
        int startPosition = TextBox_Code.SelectionStart;
        int stopPosition = startPosition + TextBox_Code.SelectionLength;
        int charCount = 0;
        for (int i = 0; i < lines.Length; i++)
        {
            charCount += lines[i].Length + Environment.NewLine.Length; //Add two bytes for line endings
            if (startPosition < charCount && !foundFirst)
            {
                first = i;
                foundFirst = true;
            }

            if (stopPosition < charCount && !foundLast)
            {
                last = i;
                foundLast = true;
            }

            if (foundFirst && foundLast)
                break;
        }
    }
    catch (Exception ex)
    {
        HandleException("GetLineRange()", ex);
    }
}

Ta metoda natomiast policzy ilość spacji w danej napisie.

private static int GetLeadingWhiteSpaceCount(string input)
{
    try
    {
        if (input == null)
            throw new ArgumentNullException("input");

        char[] line = input.ToCharArray();
        int count = 0;
        foreach (char c in line)
        {
            if (Char.IsWhiteSpace(c))
                count++;
            else
                break;
        }

        return count;
    }
    catch (Exception ex)
    {
        HandleException("GetLeadingWhiteSpaceCount()", ex);
        return 0;
    }
}

Co w ogóle nasze przyciski mają robić? Oto przykład dodawania i odejmowania akapitów czy jak ja na to mówie tabulacji.

messagebox2.gif

Oto kod do guzika, który usuwa tabulację / akapit z zaznaczonej linii tekstu. Dodatkowo przed tą operacją tutaj spacje odpowiednio zostaną zastąpione tabulacją lub zostaną usunięte całkowicie, jeśli ich ilość jest mniejsza niż wielkość tabulacji.

private void Button_DeDent_Click(object sender, RoutedEventArgs e)
{
    bool isFirstLine = true;
    int dedentedAmount = 0;
    int firstLine = 0, lastLine = 0;
    int savedSelectionLength = TextBox_Code.SelectionLength;
    int newSelectionStart = TextBox_Code.SelectionStart;
    int charCount = 0;

    SwapTabsForSpaces();

    string[] lines = ToArray(TextBox_Code.Text);

    GetLineRange(lines, ref firstLine, ref lastLine);

    for (int i = 0; i < lines.Length; i++)
    {
        if (i >= firstLine && i <= lastLine)
        {
            int leadingCount = GetLeadingWhiteSpaceCount(lines[i]);

            if (leadingCount >= TAB_SIZE)
            {
                lines[i] = lines[i].Substring(TAB_SIZE);
                dedentedAmount = TAB_SIZE;
            }
            else if (leadingCount > 0)
            {
                lines[i] = lines[i].Substring(leadingCount);
                dedentedAmount = leadingCount;
            }

            charCount += lines[i].Length + Environment.NewLine.Length;

            if (isFirstLine)
            {
                newSelectionStart = (charCount - (lines[i].Length + Environment.NewLine.Length)) + GetLeadingWhiteSpaceCount(lines[i]);
                isFirstLine = false;
            }
            else
            {
                if (savedSelectionLength > dedentedAmount)
                {
                    savedSelectionLength -= dedentedAmount;
                }

            }

            dedentedAmount = 0;
        }
        else
        {
            charCount += lines[i].Length + Environment.NewLine.Length;
        }

    }

    TextBox_Code.Text = ToText(lines);
    TextBox_Code.SelectionStart = newSelectionStart;
    TextBox_Code.SelectionLength = savedSelectionLength;
    TextBox_Code.Focus();
}

Na koniec musimy zaktualizować wskaźniki zaznaczonego tekstu, ponieważ one się zmieniły.

W użyciu widzimy tutaj także metodę ToText, która zamieni tablice linii tekstu na jeden napis.

public static string ToText(string[] lines)
{
    if (lines != null && lines.Length > 0)
    {
        var sb = new StringBuilder(lines.Length * 200);

        for (int i = 0; i < lines.Length; i++)
        {
            if (i < lines.Length - 1)
                sb.AppendLine(lines[i]);
            else
            {
                sb.Append(lines[i]);
            }
        }

        return sb.ToString();
    }
    else
    {
        return string.Empty;
    }
}

Musimy mieć też metodę, która rozbije tekst na tablice napisy rozdzieloną po nowej linii.

public static string[] ToArray(string text)
{
    if (String.IsNullOrEmpty(text) == false)
        return text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
    else
    {
        return new string[0];
    }
}

Podobna jest logika do przycisku, który dodaje akapit / tabulacje. 

private void Button_InDent_Click(object sender, RoutedEventArgs e)
{
    bool isFirstLine = true;
    int firstLine = 0, lastLine = 0;
    int savedSelectionLength = TextBox_Code.SelectionLength;
    int newSelectionStart = TextBox_Code.SelectionStart;
    int charCount = 0;

    SwapTabsForSpaces();

    string[] lines = ToArray(TextBox_Code.Text);

    GetLineRange(lines, ref firstLine, ref lastLine);

    for (int i = 0; i < lines.Length; i++)
    {
        if (i >= firstLine && i <= lastLine)
        {
            lines[i] = " ".PadLeft(TAB_SIZE) + lines[i];
            charCount += lines[i].Length + Environment.NewLine.Length;
            if (isFirstLine)
            {
                newSelectionStart = (charCount - (lines[i].Length + Environment.NewLine.Length)) + GetLeadingWhiteSpaceCount(lines[i]);
                isFirstLine = false;
            }
            else
            {
                savedSelectionLength += TAB_SIZE;
            }
        }
        else
        {
            charCount += lines[i].Length + Environment.NewLine.Length;
        }
    }

    TextBox_Code.Text = ToText(lines);
    TextBox_Code.SelectionStart = newSelectionStart;
    TextBox_Code.SelectionLength = savedSelectionLength;
    TextBox_Code.Focus();
}

Trzeba też dodać poprawkę do wciśnięcia klawisza TAB do naszego pola tekstowego

Chcemy aby pozycja wskaźnika tekstu zawsze była poprawna, a tabulacja przesuwała tekst o ten sam stały rozmiar.

private void TextBox_Code_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Tab)
    {
        //CaretIndex resets to zero after the insert - so we need to store the value here.
        int position = TextBox_Code.CaretIndex;
        TextBox_Code.Text = TextBox_Code.Text.Insert(position, String.Empty.PadLeft(TAB_SIZE));
        TextBox_Code.CaretIndex = position + TAB_SIZE;
        e.Handled = true;


    }
}

Pozostało spojrzeć nam na logikę przycisku, który poprawia wyrównanie całego tekstu. Często, gdy kopiujesz kod z innego źródła to tabulację są skopane i jest ich za dużo.

messagebox3.gif

Gdy wciskamy przycisk  znajdujemy najpierw przesunięcie tekstu na pierwszej.

Te przesuniecie określają nam jak bardzo nasz tekst jest przesunięty. Na tej podstawie wyrównamy cały tekst.

Dla pewności cały proces wykonamy ponownie tylko dla na podstawie przesuniecie ostatniej linii tekstu.

private void Button_FixIndentation_Click(object sender, RoutedEventArgs e)
{
    SwapTabsForSpaces();

    if (TextBox_Code.LineCount > 1)
    {
        string[] lines = ToArray(TextBox_Code.Text);
        int firsLineOffset = GetLeadingWhiteSpaceCount(lines[0]);
        if (firsLineOffset > 0)
            DeDent(firsLineOffset);

        lines = ToArray(TextBox_Code.Text);
        int lastLineOffset = GetLeadingWhiteSpaceCount(lines[lines.Length - 1]);
        if (lastLineOffset > 0)
            DeDent(lastLineOffset);
    }

    TextBox_Code.Focus();
}

Jak widzisz wszystkie linie tekstu są skracane o wartość offsetu, który ustaliśmy na pierwszej linii. 

private void DeDent(int amount)
{
    try
    {
        string[] lines = ToArray(TextBox_Code.Text);
        for (int i = 0; i < lines.Length; i++)
        {
            int currentOffset = GetLeadingWhiteSpaceCount(lines[i]);
            if (currentOffset >= amount)
                lines[i] = lines[i].Substring(amount);
        }

        TextBox_Code.Text = ToText(lines);
    }
    catch (Exception ex)
    {
        HandleException("DeDent()", ex);
    }
}

W całym kodzie wypadałoby na wszelki wypadek złapać każdy wyjątek, który wystąpi. 

private static void HandleException(string member, Exception ex)
{
    MessageBox.Show(String.Format("We're sorry but an error occurred in the PreCode plugin at {0}. Error message: {1}", member, ex.Message));
}

W naszej głównej klasie mamy stałą określająca rozmiar Tabulacji / Akapitu. Mamy także zmienną określającą typ kodu, który określimy w liście rozwijalnej. 

public partial class MainWindow : Window
{
    private const int TAB_SIZE = 4;
    public string SelectedCodeInText { get; set; }
    public CodeTitleDataContextModel CodeTitle { get; set; }

Mamy także DataContex, który powiążemy z kontrolką tekstową, która będzie określać tytuł naszego kodu w atrybucie HTML "data-codetitle=""".

public class CodeTitleDataContextModel
{
    private string _CodeTitle;

    public event PropertyChangedEventHandler PropertyChanged;

    public string CodeTitle
    {
        get { return _CodeTitle; }
        set
        {
            _CodeTitle = value;
            OnPropertyChanged("CodeTitle");
        }
    }

    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

Skoro mówimy o mechanizmie DataBinding to trzeba skorzystać zdarzenia "OnPropertyChanged". Teraz z każdym wpisaną literą do tego pola tekstowego nastąpi aktualizacja naszej zmiennej CodeTitle.

Jeśli chodzi o wybór typu kodu to mechanizm jest bardzo prosty. Każdy element tej listy zawiera Tag, który reprezentuje tekst, który ma być umieszczony do elementu HTML.

<StackPanel Orientation="Horizontal">
    <ComboBox    x:Name="ComboBox_Language" Width="90" Margin="7,3,0,4" Foreground="Black" HorizontalAlignment="Center"  SelectionChanged="ComboBox_SurroundWith_SelectionChanged">
        <ComboBoxItem Name="None" Tag="" Content="None"></ComboBoxItem>
        <ComboBoxItem Name="CSharp" Tag="cs" Content="CSharp"  IsSelected="True"></ComboBoxItem>
        <ComboBoxItem Name="SQL" Tag="sql" Content="SQL"></ComboBoxItem>
        <ComboBoxItem Name="HTML" Tag="html"  Content="HTML"></ComboBoxItem>
        <ComboBoxItem Name="JavaScript" Tag="js" Content="JavaScript"></ComboBoxItem>
        <ComboBoxItem Name="CSS" Tag="css"  Content="CSS"></ComboBoxItem>
        <ComboBoxItem Name="Java" Tag="java"  Content="Java"></ComboBoxItem>
        <ComboBoxItem Name="Angular" Tag="angular js"  Content="Angular"></ComboBoxItem>
        <ComboBoxItem Name="Typescript" Tag="typescript js" Content="Typescript"></ComboBoxItem>
        <ComboBoxItem Name="Xaml" Tag="xaml xml" Content="Xaml"></ComboBoxItem>
        <ComboBoxItem Name="Xml" Tag="xml" Content="Xml"></ComboBoxItem>
        <ComboBoxItem Name="objectivec" Tag="objectivec"  Content="Objectivec"></ComboBoxItem>
        <ComboBoxItem Name="Razor" Tag="razor"  Content="Razor"></ComboBoxItem>
        <ComboBoxItem Name="Json"  Tag="json"  Content="JSON"></ComboBoxItem>
        <ComboBoxItem Name="T4"   Tag="t4 cs" Content="T4"></ComboBoxItem>
        <ComboBoxItem Name="NPM" Tag="npm" Content="NPM"></ComboBoxItem>
        <ComboBoxItem Name="Fsharp" Tag="fs" Content="Fsharp"></ComboBoxItem>
        <ComboBoxItem Name="CiJava" Tag="csjava"  Content="C# i Java"></ComboBoxItem>
        <ComboBoxItem Name="Powershell" Tag="powershell" Content="Powershell"></ComboBoxItem>
        <ComboBoxItem Name="Http" Tag="http" Content="Http"></ComboBoxItem>
        <ComboBoxItem Name="COBOL" Tag="cobol" Content="COBOL"></ComboBoxItem>
        <ComboBoxItem Name="COMMANDLINEINTERFACE" Tag="cmd" Content="CMD"></ComboBoxItem>
        <ComboBoxItem Name="CPlusPlus" Tag="cpp" Content="CPlusPlus"></ComboBoxItem>
        <ComboBoxItem Name="Python" Tag="python" Content="Python"></ComboBoxItem>
        <ComboBoxItem Name="LessCSS" Tag="less" Content="Less CSS"></ComboBoxItem>
        <ComboBoxItem Name="Scss" Tag="scss" Content="Scsc"></ComboBoxItem>
    </ComboBox>
    <TextBox  Foreground="Black" Margin="7,3,0,4" Width="100"  x:Name="TXT_CodeTitle"   Text="{Binding CodeTitle, Mode=TwoWay}"  />
</StackPanel>

W zdarzeniu SelectionChanged przypisujemy Tag zaznaczonego elementu do zmiennej SelectedCodeInText.

private void ComboBox_SurroundWith_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var f = e.AddedItems[0] as ComboBoxItem;

    this.SelectedCodeInText = f.Tag.ToString();
}

Ta zmienna później zostanie użyta do określenia klasy CSS w elemencie HTML. Jest to bardzo ważne do kolorowanie składni w bibliotece JavaScript Highlight.js.

messagebox5.gif

Zobaczmy jak dodawanie tytułu kodu działa.

messagebox4.gif

To pole tekstowe musi być powiązane z właściwością "CodeTitle" w obie strony.

<TextBox  Foreground="Black" Margin="7,3,0,4" Width="100"  x:Name="TXT_CodeTitle"   Text="{Binding CodeTitle, Mode=TwoWay}"  />

DataContext jest ustawiany w C#. Na początek zmienna CodeTitle nie przechowuje nic, ale ty możesz to zmienić 

public MainWindow()
{
    Uri iconUri = new Uri("pack://application:,,,/Icon.ico", UriKind.RelativeOrAbsolute);

    this.Icon = BitmapFrame.Create(iconUri);

    InitializeComponent();

    CodeTitle = new CodeTitleDataContextModel();
    CodeTitle.CodeTitle = "";
    this.DataContext = CodeTitle;
}

Pozostało nam tylko omówić logikę ostatniego przycisku, który sformatuje tekst tak aby można było go wkleić do bloga albo jako slajd HTML.

<Button BorderBrush="#C6FA46" Padding="12,12,12,12" Background="Black" Margin="0,0,0,0" Foreground="#C6FA46" x:Name="Button_Ok"  Click="Button_Ok_Click">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Get Code to ClipArt" Padding="0,0,7,0" />
        <Image Source="icon48.png" Height="16" Width="16"  />
    </StackPanel>
</Button>

Zachowanie naszego przycisku jest zależny od chekbox-ów.

CheckBoxy będą określały :

  • Czy chcemy kodować tekst w HTML?
  • Czy chcemy uruchomić notatnik z sformatowanym już tekstem?
  • Czy chcemy otoczyć tekst tagami HTML "<pre><code>" ? 
  • Czy chcemy dodać po tekście pusty paragraf HTML <p></p> ?
<StackPanel Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
        <CheckBox IsChecked="True" x:Name="CheckBox_HtmlEncode" Margin="0,7,0,7" HorizontalAlignment="Left" Foreground="Gray">Html Encode</CheckBox>
        <CheckBox IsChecked="True" x:Name="CheckBox_AddPreCode" Margin="7,7,0,7" HorizontalAlignment="Left" Foreground="Gray">Add Pre/Code</CheckBox>
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <CheckBox IsChecked="True" x:Name="CheckBox_OpenNotepad" Margin="0,7,0,7" HorizontalAlignment="Left" Foreground="Gray">OpenNotpad</CheckBox>
        <CheckBox IsChecked="True" x:Name="CheckBox_AddParagraf" Margin="7,7,0,7" HorizontalAlignment="Left" Foreground="Gray">Add &lt;p&gt;</CheckBox>
        <CheckBox x:Name="CheckBox_LineEndings" Margin="0,7,0,7" Visibility="Collapsed" HorizontalAlignment="Left" Foreground="Gray">Swap CRLFs with &lt;BR&gt;</CheckBox>
    </StackPanel>
</StackPanel>

W zależności od zaznaczonych checkboxów robimy różne rzeczy.

private void Button_Ok_Click(object sender, RoutedEventArgs e)
{
    if (!String.IsNullOrEmpty(TextBox_Code.Text))
    {
        //Clean tabs out
        SwapTabsForSpaces();

        Code = TextBox_Code.Text;

        //Encode
        if (CheckBox_HtmlEncode.IsChecked.HasValue && CheckBox_HtmlEncode.IsChecked.Value)
            Code = HttpUtility.HtmlEncode(Code);

        //Swap line endings for </br>
        if (CheckBox_LineEndings.IsChecked.HasValue && CheckBox_LineEndings.IsChecked.Value)
            Code = Code.Replace(Environment.NewLine, "<br />");

        //Code = $"<pre><code class=\"hljs {SelectedCodeInText} \">\n\r{Code}\n\r</code></pre>"
        ;

        if (CheckBox_AddPreCode.IsChecked == false)
        {
            Clipboard.SetText(Code);
            if (CheckBox_OpenNotepad.IsChecked == true)
                NotepadHelper.ShowMessage(Code);
            return;
        }

        if (CodeTitle.CodeTitle.Length > 1)
            Code = $"<pre><code data-codetitle=\"{CodeTitle.CodeTitle}\" class=\"hljs {SelectedCodeInText} \">{Code}</code></pre>";
        else
            Code = $"<pre><code class=\"hljs {SelectedCodeInText} \">{Code}</code></pre>";

        if (CheckBox_AddParagraf.IsChecked == true)
            Code = $"<p>{Code}</p><p>...</p>";

        Clipboard.SetText(Code);
        if (CheckBox_OpenNotepad.IsChecked == true)
            NotepadHelper.ShowMessage(Code);

    }


}

Ostatecznie zawsze tekst jest umieszczany do schowka poprzez metodę : Clipboard.SetText().

Clipboard.SetText(Code);
if (CheckBox_OpenNotepad.IsChecked == true)
    NotepadHelper.ShowMessage(Code);

Jeśli checkbox od notatnika jest zaznaczony to otwieramy notatnik.

Dodatkowo sprawdzam rozmiar wielkości napisu tytułu kodu. Jeśli jest on większy niż 1 to uznaje, że trzeba dodać atrybut HTML "data-codetitle="""

if (CodeTitle.CodeTitle.Length > 1)
    Code = $"<pre><code data-codetitle=\"{CodeTitle.CodeTitle}\" class=\"hljs {SelectedCodeInText} \">{Code}</code></pre>";
else
    Code = $"<pre><code class=\"hljs {SelectedCodeInText} \">{Code}</code></pre>";

To wszystko, jeśli chodzi o tą aplikację. Nie ma tutaj genialnych wzorów czy specjalnych własnoręcznie pisanych kontrolek XAML. Ważne jednak, że program działa i nawet pisząc ten wpis ten program pomógł mi dodać kod XAML i C# do tego wpisu. 

Ikona programu do formatowania kodu

Jak utworzyć jeden plik EXE do tego programu?

Możesz przeczytać o tym tutaj : https://cezarywalenciuk.pl/blog/programing/winforms-wpf-jak-publikowac-aplikacje-jako-przenosny-plik

Jeśli się zastanawiasz, dlaczego ten program waży 142 MB to wynika z tego, że zależało mi na tym , aby stworzyć plik ".exe", który zadziała niezależnie od tego, czy użytkownik ma zainstalowany .NET 5, czy nie.

Kod można pobrać tutaj : GitHub - PanNiebieski/PreCodeTextFormaterWPF