вторник, 20 декабря 2011 г.

Готовим вкусный блокнот из AvalonEdit и AvalonDock

Всем привет. Сегодня хочу поделиться опытом работы с контролами AvalonEdit и AvalonDock
Итак, моя цель - сделать блокнот с подсветкой синтаксиса, intellisense и оконным интерфейсом, как у Visual Studio 2010.

В качестве оправной точки я использую статьи по работе с данными контролами (Using AvalonEdit (WPF Text Editor), WPF Docking Library). Ознакомившись с данным материалом, я начал с оконного интерфейса. Это оказалось очень просто:
Используем DockingManager на главной странице:
  1. <ad:DockingManager x:Name="DockManager" IsAnimationEnabled="True" Grid.Row="1" Margin="0,0,0,0"
  2.              ActiveContent="{Binding ElementName=Content1}" DocumentClosing="DockManagerDocumentClosing" >
  3.     <ad:ResizingPanel Orientation="Horizontal">
  4.         <ad:DocumentPane x:Name="DocumentHost">
  5.         </ad:DocumentPane>
  6.     </ad:ResizingPanel>
  7. </ad:DockingManager>
И DocumentContent для дочерних документов
  1. <ad:DocumentContent x:Class="DotNetNotepad.UI.Document"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.              xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
  7.                     xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
  8.                      mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
  9.     <Grid Background="White">
Полный листинг я приводить не буду, так как исходники всё равно опубликую, а загромождать кодом блог нет смысла.
После этих манипуляций, можно получить что то вроде вот этого
Далее необходимо добавить главное меню. Там будет всего пара пунктов

  1. <Menu>
  2.     <MenuItem Header="File">
  3.         <MenuItem Header="New" Click="NewClick">
  4.         </MenuItem>
  5.         <MenuItem Header="Open" Click="OpenFileClick">
  6.         </MenuItem>
  7.         <Separator/>
  8.         <MenuItem Header="Exit" Click="ExitClick">
  9.         </MenuItem>
  10.     </MenuItem>
  11. </Menu>
Названия элементов говорят сами за себя. Следующий шаг - добавление с закладки текстового редактора. Это делается просто - сначала добавляем панель инструментов, потом сам редактор:


  1. <ad:DocumentContent x:Class="DotNetNotepad.UI.Document"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.              xmlns:ad="clr-namespace:AvalonDock;assembly=AvalonDock"
  7.                     xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
  8.                      mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
  9.     <Grid>
  10.  
  11.         <Grid.RowDefinitions>
  12.             <RowDefinition Height="32"/>
  13.             <RowDefinition/>
  14.         </Grid.RowDefinitions>
  15.         <ToolBar DockPanel.Dock="Top">
  16.             <ToolBar.Resources>
  17.                 <Style TargetType="{x:Type Image}">
  18.                     <Style.Triggers>
  19.                         <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}, AncestorLevel=1}, Path=IsEnabled}" Value="False">
  20.                             <Setter Property="Opacity" Value="0.30" />
  21.                         </DataTrigger>
  22.                     </Style.Triggers>
  23.                 </Style>
  24.             </ToolBar.Resources>
  25.             <Button Click="SaveFile" >
  26.                 <Image Source="Images/Save.png" Height="16"/>
  27.             </Button>
  28.             <Separator/>
  29.             <Button Command="Cut">
  30.                 <Image Source="Images/Cut.png" Height="16"/>
  31.             </Button>
  32.             <Button Command="Copy">
  33.                 <Image Source="Images/Copy.png" Height="16"/>
  34.             </Button>
  35.             <Button Command="Paste">
  36.                 <Image Source="Images/Paste.png" Height="16"/>
  37.             </Button>
  38.             <Button Command="Delete">
  39.                 <Image Source="Images/Delete.png" Height="16"/>
  40.             </Button>
  41.             <Separator/>
  42.             <Button Command="Undo">
  43.                 <Image Source="Images/Undo.png" Height="16"/>
  44.             </Button>
  45.             <Button Command="Redo">
  46.                 <Image Source="Images/Redo.png" Height="16"/>
  47.             </Button>
  48.             <Separator/>
  49.             <CheckBox IsChecked="{Binding ElementName=textEditor,Path=WordWrap}">
  50.                 <Image Source="Images/WordWrap.png" Height="16"/>
  51.             </CheckBox>
  52.             <CheckBox IsChecked="{Binding ElementName=textEditor,Path=ShowLineNumbers}">
  53.                 <TextBlock Width="16" TextAlignment="Center">#</TextBlock>
  54.             </CheckBox>
  55.             <CheckBox IsChecked="{Binding ElementName=textEditor,Path=Options.ShowEndOfLine}">
  56.                 <TextBlock Width="16" TextAlignment="Center">¶</TextBlock>
  57.             </CheckBox>
  58.             <!--Тут происходит биндинг вариантов подсветки синтаксиса, которые загружены в редактор-->
  59.             <ComboBox Name="highlightingComboBox"
  60.                 SelectedItem="{Binding SyntaxHighlighting, ElementName=textEditor}"
  61.                 ItemsSource="{Binding Source={x:Static avalonedit:HighlightingManager.Instance}, Path=HighlightingDefinitions}"
  62.                 SelectionChanged="HighlightingComboBoxSelectionChanged"/>
  63.         </ToolBar>
  64.         <!--По умолчанию редактор настроен на подсветку XML-->
  65.         <avalonedit:TextEditor    
  66.              x:Name="textEditor"
  67.              FontFamily="Consolas"
  68.              FontSize="10pt"
  69.              ShowLineNumbers="True"            
  70.              SyntaxHighlighting="XML"
  71.             Grid.Row="1">
  72.             <avalonedit:TextEditor.ContextMenu>
  73.                 <ContextMenu>
  74.                     <MenuItem Command="Cut" Header="Cut" />
  75.                     <MenuItem Command="Copy" Header="Copy" />
  76.                     <MenuItem Command="Paste" Header="Paste" />
  77.                     <MenuItem Command="Delete" Header="Delete" />
  78.                     <MenuItem Command="Undo" Header="Undo" />
  79.                     <MenuItem Command="Redo" Header="Redo" />
  80.                 </ContextMenu>
  81.             </avalonedit:TextEditor.ContextMenu>
  82.         </avalonedit:TextEditor>
  83.     </Grid>
  84. </ad:DocumentContent>
Следует пояснить назначение одного из обработчиков, указанных в листинге

  1. /// <summary>
  2. /// Позволяет устанавливать стратегии разбиения текста на части
  3. /// </summary>
  4. FoldingManager _foldingManager;
  5. /// <summary>
  6. /// Сама стратегия разбиения
  7. /// </summary>
  8. AbstractFoldingStrategy _foldingStrategy;
  9.  
  10. /// <summary>
  11. /// Обработчик меняет стратегию разбиения текста на части. Подсветка синтаксиса меняется автоматом.
  12. /// </summary>
  13. /// <param name="sender"></param>
  14. /// <param name="e"></param>
  15. void HighlightingComboBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
  16. {
  17.     // SyntaxHighlighting - это свойство, определяющее текущее правило подсветки синтаксиса
  18.     if (textEditor.SyntaxHighlighting == null)
  19.     {
  20.         _foldingStrategy = null;
  21.     }
  22.     else
  23.     {
  24.         switch (textEditor.SyntaxHighlighting.Name)
  25.         {
  26.             case "XML":
  27.                 _foldingStrategy = new XmlFoldingStrategy();
  28.                 textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.DefaultIndentationStrategy();
  29.                 break;
  30.             case "C#":
  31.             case "C++":
  32.             case "PHP":
  33.             case "Java":
  34.                 textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.CSharp.CSharpIndentationStrategy(textEditor.Options);
  35.                 _foldingStrategy = null;
  36.                 break;
  37.             default:
  38.                 textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.DefaultIndentationStrategy();
  39.                 _foldingStrategy = null;
  40.                 break;
  41.         }
  42.     }
  43.     if (_foldingStrategy != null)
  44.     {
  45.         if (_foldingManager == null)
  46.             _foldingManager = FoldingManager.Install(textEditor.TextArea);
  47.         _foldingStrategy.UpdateFoldings(_foldingManager, textEditor.Document);
  48.     }
  49.     else
  50.     {
  51.         if (_foldingManager != null)
  52.         {
  53.             FoldingManager.Uninstall(_foldingManager);
  54.             _foldingManager = null;
  55.         }
  56.     }
  57. }
После этих работ, блокнот стал выглядеть весьма привлекательно
Однако, как оказалось, в AvalonEdit не встроена поддержка подсветки SQL и он не может автоматически распознать, подсветку чего включить при открытии файла.
Первая проблема решилась очень быстро - я просто погуглил, и нашел определение синтаксиса для SQL в одном из проектов на кодеплексе, и мне осталось лишь его встроить в редактор.
  1. <?xml version="1.0"?>
  2. <!--
  3. Copyright 2005-2009 Paul Kohler (http://pksoftware.net/MiniSqlQuery/). All rights reserved.
  4. This source code is made available under the terms of the Microsoft Public License (Ms-PL)
  5. http://minisqlquery.codeplex.com/license
  6. -->
  7. <SyntaxDefinition name = "SQL" extensions = ".sql">
  8.     <Properties>
  9.         <Property name="LineComment" value="--"/>
  10.     </Properties>
  11.     <Digits name = "Digits" bold = "true" italic = "false" color = "Blue"/>
  12.     <RuleSets>
  13.         <RuleSet ignorecase = "true">
  14.             <Delimiters>&amp;&lt;&gt;~!%^*()-+=|\#/{}[]:;"' ,    .?</Delimiters>
  15.             <Span name="String" bold="false" italic="false" color="Red" stopateol="false">
  16.                 <Begin>'</Begin>
  17.                 <End>'</End>
  18.             </Span>
  19.             <Span name = "LineComment" bold = "false" italic = "false" color = "Green" stopateol = "true">
  20.                 <Begin>--</Begin>
  21.             </Span>
  22.             <Span name = "BlockComment" bold = "false" italic = "false" color = "Green" stopateol = "false">
  23.                 <Begin>/*</Begin>
  24.                 <End>*/</End>
  25.             </Span>
  26.             <KeyWords name="JoinKeywords" bold="true" italic="false" color="Purple">
  27.                 <Key word="INNER" />
  28.                 <Key word="JOIN" />
  29.                 <Key word="LEFT" />
  30.                 <Key word="RIGHT" />
  31.                 <Key word="OUTER" />
  32.                 <Key word="UNION" />
  33.             </KeyWords>
  34.             <KeyWords name="AliasKeywords" bold="false" italic="false" color="Maroon">
  35.                 <Key word="AS" />
  36.             </KeyWords>
  37.             <KeyWords name="ComparisonKeywords" bold="true" italic="false" color="Navy">
  38.                 <Key word="AND" />
  39.                 <Key word="OR" />
  40.                 <Key word="LIKE" />
  41.             </KeyWords>
  42.             <KeyWords name="SpecializedKeywords" bold="true" italic="false" color="Gray">
  43.                 <Key word="TOP" />
  44.                 <Key word="LIMIT" />
  45.                 <Key word="OPENDATASOURCE" />
  46.                 <Key word="GO" />
  47.             </KeyWords>
  48.             <KeyWords name="DestructiveKeywords" bold="true" italic="false" color="Red">
  49.                 <Key word="DROP" />
  50.                 <Key word="DELETE" />
  51.                 <Key word="TRUNCATE" />
  52.             </KeyWords>
  53.             <KeyWords name="SqlKeywordsBold" bold="true" italic="false" color="Blue">
  54.                 <Key word="BEGIN" />
  55.                 <Key word="END" />
  56.                 <Key word="EXEC" />
  57.                 <Key word="CREATE" />
  58.                 <Key word="COMMIT" />
  59.                 <Key word="RAISERROR" />
  60.                 <Key word="ROLLBACK" />
  61.                 <Key word="TRAN" />
  62.                 <Key word="TRANSACTION" />
  63.                 <Key word="USE" />
  64.                 <Key word="USER" />
  65.                 <Key word="VIEW" />
  66.             </KeyWords>
  67.             <KeyWords name="SqlKeywordsNormal" bold="false" italic="false" color="Blue">
  68.                 <Key word="ADD" />
  69.                 <Key word="ALL" />
  70.                 <Key word="ANY" />
  71.                 <Key word="ASC" />
  72.                 <Key word="BETWEEN" />
  73.                 <Key word="BREAK" />
  74.                 <Key word="BY" />
  75.                 <Key word="CASCADE" />
  76.                 <Key word="CASE" />
  77.                 <Key word="CHECK" />
  78.                 <Key word="CHECKPOINT" />
  79.                 <Key word="CLOSE" />
  80.                 <Key word="COALESCE" />
  81.                 <Key word="COLLATE" />
  82.                 <Key word="COLUMN" />
  83.                 <Key word="COMPUTE" />
  84.                 <Key word="CONSTRAINT" />
  85.                 <Key word="CONTAINS" />
  86.                 <Key word="CONTINUE" />
  87.                 <Key word="CONVERT" />
  88.                 <Key word="CROSS" />
  89.                 <Key word="CURSOR" />
  90.                 <Key word="DECLARE" />
  91.                 <Key word="DEFAULT" />
  92.                 <Key word="DESC" />
  93.                 <Key word="DISTINCT" />
  94.                 <Key word="DOUBLE" />
  95.                 <Key word="ELSE" />
  96.                 <Key word="ESCAPE" />
  97.                 <Key word="EXCEPT" />
  98.                 <Key word="EXECUTE" />
  99.                 <Key word="EXISTS" />
  100.                 <Key word="EXIT" />
  101.                 <Key word="FETCH" />
  102.                 <Key word="FOR" />
  103.                 <Key word="FROM" />
  104.                 <Key word="FULL" />
  105.                 <Key word="FUNCTION" />
  106.                 <Key word="GOTO" />
  107.                 <Key word="GROUP" />
  108.                 <Key word="HAVING" />
  109.                 <Key word="IDENTITY" />
  110.                 <Key word="IDENTITY_INSERT" />
  111.                 <Key word="IDENTITYCOL" />
  112.                 <Key word="IF" />
  113.                 <Key word="IN" />
  114.                 <Key word="INSERT" />
  115.                 <Key word="INTO" />
  116.                 <Key word="IS" />
  117.                 <Key word="KEY" />
  118.                 <Key word="NOCHECK" />
  119.                 <Key word="NOT" />
  120.                 <Key word="NULL" />
  121.                 <Key word="NULLIF" />
  122.                 <Key word="OF" />
  123.                 <Key word="OFF" />
  124.                 <Key word="OFFSETS" />
  125.                 <Key word="ON" />
  126.                 <Key word="OPEN" />
  127.                 <Key word="ORDER" />
  128.                 <Key word="OVER" />
  129.                 <Key word="PRECISION" />
  130.                 <Key word="PROC" />
  131.                 <Key word="PROCEDURE" />
  132.                 <Key word="PUBLIC" />
  133.                 <Key word="READ" />
  134.                 <Key word="READTEXT" />
  135.                 <Key word="REFERENCES" />
  136.                 <Key word="RESTORE" />
  137.                 <Key word="RESTRICT" />
  138.                 <Key word="RETURN" />
  139.                 <Key word="ROWCOUNT" />
  140.                 <Key word="RULE" />
  141.                 <Key word="SAVE" />
  142.                 <Key word="SELECT" />
  143.                 <Key word="SET" />
  144.                 <Key word="SETUSER" />
  145.                 <Key word="SOME" />
  146.                 <Key word="TABLE" />
  147.                 <Key word="THEN" />
  148.                 <Key word="TO" />
  149.                 <Key word="TRIGGER" />
  150.                 <Key word="UNIQUE" />
  151.                 <Key word="UPDATE" />
  152.                 <Key word="VALUES" />
  153.                 <Key word="VARYING" />
  154.                 <Key word="WAITFOR" />
  155.                 <Key word="WHEN" />
  156.                 <Key word="WHERE" />
  157.                 <Key word="WHILE" />
  158.                 <Key word="WITH" />
  159.             </KeyWords>
  160.         </RuleSet>
  161.     </RuleSets>
  162. </SyntaxDefinition>
Так как все определения синтаксиса, указанные в подобных файлах, содержат список расширений файлов, которые ассоциируются с определением, то среди классов библиотеки AvalonEdit я легко нашел тот, что поможет мне получить определение из имени файла. Таким образом, добавить автоматическое определение синтаксиса по расширению открываемого файла в блокнот оказалось делом тривиальным.
  1. var ext = Path.GetExtension(fileName);
  2. if (ext != null)
  3. {
  4.     var def = HighlightingManager.Instance.GetDefinitionByExtension(ext);
  5.     if (def != null)
  6.     {
  7.         textEditor.SyntaxHighlighting = def;
  8.     }
  9. }
Написав совсем немного кода, мне удалось сделать так, что можно добавлять файлы определения синтаксиса в уже готовую программу, просто положив их в определенную папку.
Следующей моей задачей было прикрутить intellisense. Так как я в основном пользуюсь C# и SQL, то мне будет достаточно сделать интелисенс только для них. Для себя я представил реализацию как просто набор ключевых слов синтаксиса, которые фильтруются по мере того, как пользователь набирает слово. Наборы ключевых слов я просто взял из MSDN (C# 2010, SQL Server 2008) и разделил их на 2 файла соответственно. Затем мне нужно было осуществить привязку текущего правила подсветки синтаксиса и набора ключевых слов. Это я сделал при помощи простого XML файла
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <ArrayOfKeywordsFileOption xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  3.     <KeywordsFileOption HighlightingName="SQL" Filename="keywords_sql.txt" />
  4.     <KeywordsFileOption HighlightingName="C#" Filename="keywords_csharp.txt" />
  5. </ArrayOfKeywordsFileOption>
Для загрузки правил из этого файла я, как легко догадаться, использую XML десериализацию. Это очень удобно - благодаря этому XML файлу я могу добавлять/удалять ключевые слова для любого правила подсветки синтаксиса, которое я также могу добавить в будущем, и всё это без перекомпиляции программы.
Дальше всё просто - мне достаточно отследить пару событий текстового редактора, определить, стоит ли мне показывать окно с ключевыми словами - и если стоит, то получить список ключевых слов и отобразить его.

  1. private CompletionWindow _completionWindow;
  2. void TextEditorTextAreaTextEntered(object sender, TextCompositionEventArgs e)
  3. {
  4.     ICompletionWindowResolver resolver = new CompletionWindowResolver(textEditor.Text, textEditor.CaretOffset, e.Text, textEditor);
  5.     _completionWindow = resolver.Resolve();
  6. }
  7.  
  8. void TextEditorTextAreaTextEntering(object sender, TextCompositionEventArgs e)
  9. {
  10.     if (e.Text.Length > 0 && _completionWindow != null)
  11.     {
  12.         if (!char.IsLetterOrDigit(e.Text[0]))
  13.         {
  14.             _completionWindow.CompletionList.RequestInsertion(e);
  15.         }
  16.     }
  17. }
Вот интерфейс класса CompletionWindowResolver
  1. public interface ICompletionWindowResolver
  2. {
  3.     CompletionWindow Resolve();
  4. }
Как видно, тут необходимо просто определиться с окном intellisense - получить его экземпляр и отобразить. Вот моя реализация метода Resolve

  1. public CompletionWindow Resolve()
  2. {
  3.  
  4.     var hiName = string.Empty;
  5.     if (_target.SyntaxHighlighting != null)
  6.     {
  7.         hiName = _target.SyntaxHighlighting.Name;
  8.     }
  9.  
  10.     var cdata = _dataProviders.SelectMany(x => x.GetData(_text, _position, _input, hiName)).ToList();
  11.     int count = cdata.Count;
  12.     if (count > 0)
  13.     {
  14.         var completionWindow = new CompletionWindow(_target.TextArea);
  15.  
  16.         var data = completionWindow.CompletionList.CompletionData;
  17.  
  18.         foreach (var completionData in cdata)
  19.         {
  20.             data.Add(completionData);
  21.         }
  22.  
  23.         completionWindow.Show();
  24.         completionWindow.Closed += delegate
  25.                                        {
  26.                                            completionWindow = null;
  27.                                        };
  28.         return completionWindow;
  29.  
  30.     }
  31.     return null;
  32. }
Для пояснения
  1. private readonly List<ICompletionDataProvider> _dataProviders = new List<ICompletionDataProvider>();
И мой интерфейс провайдера
  1. public interface ICompletionDataProvider
  2. {
  3.     IEnumerable<ICompletionData> GetData(string text, int position, string input, string highlightingName);
  4. }
Таким образом я могу определить всю логику, по которой строится список подсказок в окне, что даёт полную свободу действий.
В результате всех этих манипуляций, у меня получился блокнот с поддержкой пдсветки синтаксиса, автодополнением и удобными закладками. Иконку для приложения я взял среди бесплатных и выложил проект на кодеплекс.

Это всё. Всем спасибо.

17 комментариев:

  1. А эту штука может работать с файлом, в котором смешаны 2 синтаксиса? Например, PHP и HTML.

    ОтветитьУдалить
  2. Одновременно может поддерживаться только одно правило подсветки синтаксиса на один документ. Но ничто не мешает самому определить правила подсветки синтаксиса. Это означает, что подсветить можно что угодно и как угодно.

    ОтветитьУдалить
  3. Отличная статья, а как вы осуществили выпадающие меню при нажатии на Ctrl+Space?

    ОтветитьУдалить
    Ответы
    1. Этот функционал встроен в AvalonEdit, мне осталось только подписаться на нужные события.

      Удалить
    2. А незнаиет под Windows 8 METRO есть подобный контрол?

      Удалить
    3. К сожалению под windows 8 я ещё не начал писать :)

      Удалить
  4. Как Вы осуществили создание нового документа?

    ОтветитьУдалить
    Ответы
    1. Спасибо очень полезно.
      Не смог найти как в AvalonEdit подчеркнуть слово.
      Есть ли такой сервис?

      Удалить
    2. Добрый день. К сожалению, не могу ответить на вопрос. Даже если бы и знал ответ, прошло уже полтора года с момента публикации, многое уже могло измениться. Однако, это же WPF, возможно, можно подчеркнуть текст штатными средствами.

      Удалить
    3. Да, в AvalonEdit можно подчёркивать слова.
      Вот пример текстового редактора http://poet.of.by/ru/, в котором реализована проверка орфографии с выделением ошибок и много других интересных вещей.

      Код программы пока что закрыт, но я могу проконсультировать желающих по некоторым техническим деталям. Контакты есть на сайте.

      Всего вам наилучшего

      Удалить
  5. А вы не подскажите кка создать правило, которое будет работать на подобии интерпретатора html, читать строки с тегами, например `Description: This is an item number 1.` и выводить форматированный текст, опуская теги?

    ОтветитьУдалить
    Ответы
    1. Не уверен, что понял вопрос. Куда выводить? В посте нет преобразований текста, тут только подсветка и автокомплит.

      Удалить