Практикум прикладного программирования на C# в среде VS.NET 2008

Разработка компонента WPF и анализатора HTML-таблиц

Разбить на страницы
Показывать лекцию целиком

Упражнение 1. Создание компонента WPF "Цифровой таймер"

Все необходимые для выполнения данной работы программы можно найти в Setup.zip.

В WPF предусмотрены два вида пользовательских элементов:

  • Пользовательские элементы управления - в Visual Studio это тип проекта WPF User Control Library
  • Настраиваемые пользовательские элементы - в Visual Studio это тип проекта WPF Custom Control Library
  • Настраиваемые пользовательские элементы невидимы. Для них проектная плоскость Design с возможностью перетаскивания элементов из панели Toolbox не предусмотрена, поэтому надо создавать шаблон. А пользовательские элементы управления имеют проектную плоскость времени проектирования и их можно создавать точно также, как проект интерфейса приложения WPF Application. Но запускать на выполнение пользовательские элементы управления можно только с помощью исполнимого теста.

    Поставим задачу разработать секундомер, показывающий время, оставшееся до наступления какого-нибудь события. Объект таймера оформим как пользовательский элемент управления, помещенный в библиотеку типа WPF User Control Library. При построении используем механизмы ресурсов и привязки WPF.

    Создание таймера в библиотеке

  • Создайте решение ClockAndConvertHTML и новый проект библиотеки WpfControlLibrary, используя шаблон WPF User Control Library в типе проектов Visual Studio
  • В Solution Explorer переименуйте автоматически сгенерированный файл UserControl1.xaml в Clock.xaml
  • В редакторе файла Clock.xaml на вкладке XAML жестом Ctrl+H вызовите окно замены с настройкой Current Project и замените кнопкой Replace All все вхождения UserControl1 на Clock
  • В панели Solution Explorer вызовите контекстное меню для корневого узла WpfControlLibrary проекта и добавьте папку Code командой Add/New Folder
  • В панели Solution Explorer вызовите контекстное меню для папки Code и командой Add/Class добавьте в нее файл с именем DigitalClock.cs, который заполните так
  • using System;
    using System.Windows;           // DependencyObject, DependencyProperty
    using System.ComponentModel;    // INotifyPropertyChanged
    using System.Windows.Threading; // DispatcherTimer
    using System.Globalization;     // CultureInfo 
    using System.Windows.Controls;  // TextBlock
    using System.Windows.Media;     // FontFamily
        
    namespace MyClock
    {
        public class DigitalClock : TextBlock, INotifyPropertyChanged
        {
            const int COUNT_TIME = 10;    // Минуты
        
            // Событие реализации интерфейса INotifyPropertyChanged
            public event PropertyChangedEventHandler PropertyChanged;
        
            // Обычное событие остановки таймера
            public event EventHandler StopDigitalClock;
        
            // Открытое свойство 
            int minutes, seconds, time;
            public TextBlock Time
            {
                get
                {
                    minutes = time / 60;
                    seconds = time % 60;
                    this.Text = String.Format("{0:00}:{1:D2}", minutes, seconds);
                    if (time > 0)
                        time -= 1;
                    return this;
                }
            }
        
            // Конструктор запускает таймер
            DispatcherTimer timer;
            public DigitalClock()
            {
                // Настраиваем дизайн
                this.FontFamily = new FontFamily("Arial");
                this.FontSize = 1.3333 * 36;    // 36pt
                this.FontWeight = FontWeights.Bold;
                this.Background = Brushes.Red;
                this.Foreground = Brushes.Yellow;
                this.Padding = new Thickness(5, 0, 5, 0);
        
                // Создаем таймер
                timer = new DispatcherTimer();
                timer.Tick += new EventHandler(timer_Tick);
                timer.Interval = TimeSpan.FromSeconds(1);
        
                // Начальное значение
                time = COUNT_TIME * 60;   // Секунды
            }
        
            public void Start()
            {
                timer.Stop();
                timer.Start();
            }
        
            public void Restart()
            {
                time = COUNT_TIME * 60;   // Секунды
                timer.Stop();
                timer.Start();
            }
        
            public void StopResult()
            {
                timer.Stop();
                if (StopDigitalClock != null)
                    StopDigitalClock(this, EventArgs.Empty);
            }
        
            public void Stop()
            {
                timer.Stop();
            }
        
            // Обработчик события таймера возбуждает свойство Time
            // и генерирует обычное событие останова 
            void timer_Tick(object sender, EventArgs e)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Time"));
        
                if (minutes <= 0  seconds <= 0)
                {
                    timer.Stop();
                    if (StopDigitalClock != null)
                        StopDigitalClock(this, EventArgs.Empty);
                }
            }
        }
    }

    Класс возбуждает обычное событие StopDigitalClock, когда заканчивается лимит времени.

  • Откройте на редактирование файл Clock.xaml в представлении XAML и заполните его следующей разметкой
  • <UserControl x:Class="WpfControlLibrary.Clock"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyClock"
        >
        <UserControl.Resources>
            <local:DigitalClock x:Key="clock" StopDigitalClock="DigitalClock_StopDigitalClock" />
        </UserControl.Resources>
        <Label Content="{Binding Source={StaticResource clock}, Path=Time}"
               BorderBrush="Blue" BorderThickness="5" Padding="0"
               HorizontalAlignment="Center" />
    </UserControl>
  • Откройте на редактирование файл Clock.xaml.cs и заполните его следующим процедурным кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    using MyClock; // Пространство имен таймера в библиотеке WpfControlLibrary
        
    namespace WpfControlLibrary
    {
        public partial class Clock : UserControl
        {
            public Clock()
            {
                InitializeComponent();
        
                // Извлекаем объект из коллекции ресурсов
                clock = this.Resources["clock"] as DigitalClock;
            }
            DigitalClock clock; // Ссылка на экземпляр из ресурсов
        
            // Поле маршрутизированного события
            public static readonly RoutedEvent StopClockEvent;
        
            // Статический конструктор
            static Clock()
            {
                // Регистрация маршрутизированного события
                StopClockEvent = EventManager.RegisterRoutedEvent(
                    "StopClock", RoutingStrategy.Bubble,
                    typeof(RoutedEventHandler), typeof(Clock));
            }
        
            // Контейнер для подписки на маршрутизированное событие
            public event RoutedEventHandler StopClock
            {
                add { AddHandler(StopClockEvent, value); }
                remove { RemoveHandler(StopClockEvent, value); }
            }
        
            // Возбуждение маршрутизированного события через обработчик простого события
            private void DigitalClock_StopDigitalClock(object sender, EventArgs e)
            {
                this.RaiseEvent(new RoutedEventArgs(Clock.StopClockEvent));
            }
        
            // Функции доступа для управления таймером из клиента
            public void Start()
            {
                clock.Start();
            }
            public void Restart()
            {
                clock.Restart();
            }
            public void StopResult()
            {
                clock.StopResult();
            }
            public void Stop()
            {
                clock.Stop();
            }
        }
    }

    Обычное событие StopDigitalClock через обработчик возбуждает всплывающее событие StopClock, которое в клиенте можно будет слушать и управлять интерфейсом. Для управления таймером в классе Clock предусмотрены функции доступа.

  • Выполните команду меню Build/Build Solution или жест F6, чтобы создать библиотечную сборку
  • В панели Solution Explorer щелкните на пиктограмме Show All Files и убедитесь, что сборка WpfControlLibrary.dll действительно создана
  • Создание тестового приложения

    Проверим работу созданного таймера с помощью тестирующего приложения.

  • Командой меню оболочки File/Add/New Project добавьте к решению исполнимую сборку типа WPF Application с именем TestClock
  • В панели Solution Explorer вызовите контекстное меню для узла TestClock и командой Set as StartUp Project назначьте его стартовым. Обратите внимание, что после выполнение этой команды узел стартового проекта выделился полужирным стилем
  • В Solution Explorer выполните для узла References проекта TestClock команду Add Reference контекстного меню и на вкладке Projects подключите библиотечную сборку WpfControlLibrary.dll к тестовому проекту
  • В Solution Explorer выделите любой узел тестового проекта TestClock, щелкните на пиктограмме Show All Files и раскройте узел bin\Debug - убедитесь, что пока он пустой
  • Откомпилируйте тестовый проект и щелкните в Solution Explorer на пиктограмме Refresh - убедитесь, что оболочка скопировала в каталог размещения тестовой сборки нашу библиотечную сборку WpfControlLibrary.dll
  • Откройте на редактирование файл Window1.xaml в представлении XAML и заполните его следующей разметкой
  • <Window x:Class="TestClock.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lib="clr-namespace:WpfControlLibrary;assembly=WpfControlLibrary"
        Title="TestClock" Height="300" Width="300"
        lib:Clock.StopClock="Window_StopClock" 
            >
        <StackPanel lib:Clock.StopClock="StackPanel_StopClock">
            <lib:Clock Name="timer" />
            <Button Click="start_Click">Start</Button>
            <Button Click="restart_Click">Restart</Button>
            <Button Click="stopResult_Click">StopResult</Button>
            <Button Click="stop_Click">Stop</Button>
        </StackPanel>
    </Window>
  • Заполните файл Window1.xaml.cs следующим процедурным кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace TestClock
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            private void start_Click(object sender, RoutedEventArgs e)
            {
                timer.Start();
            }
        
            private void restart_Click(object sender, RoutedEventArgs e)
            {
                timer.Restart();
            }
        
            private void stopResult_Click(object sender, RoutedEventArgs e)
            {
                timer.StopResult();
            }
        
            private void stop_Click(object sender, RoutedEventArgs e)
            {
                timer.Stop();
            }
        
            private void Window_StopClock(object sender, RoutedEventArgs e)
            {
                MessageBox.Show("StopClock", "Window1");
            }
        
            private void StackPanel_StopClock(object sender, RoutedEventArgs e)
            {
                MessageBox.Show("StopClock", "StackPanel");
            }
        }
    }

    На прослушивание всплывающего события StopClock мы в разметке подписали два интерфейсных элемента: Window и StackPanel, которые в своих обработчиках выдают диалоговое окно MessageBox. Это позволит проследить прохождение нашего события, чтобы убедиться, что оно действительно всплывает.

  • Запустите тестовый проект и испытайте работу таймера - функционирует как и задумали
  • Таким образом, мы спроектировали простой цифровой таймер и упаковали его в библиотеку. Для проверки работы таймера разработали тестирующую исполнимую сборку и подключили к ней библиотечную сборку. И все работает. Пример простой, но показательный. В дальнейшем, при развитии текущего решения или разработке новых проектов можно добавлять в ту же самую библиотеку и другие элементы управления пользовательского интерфейса.

    Упражнение 2. Разработка утилиты преобразования HTML-таблиц <table> в объекты DataSet

    Для начала используем программные тесты с кодом HTML. Рассмотрим два способа разбора XML -кода. Для реализации этих способов воспользуемся консольным приложением.

  • Командой меню File/Add/New Project добавьте к решению новый проект с именем ConvertTables и назначьте его стартовым
  • Оболочка автоматически создала файл Program.cs с точкой входа в приложение.

    Пример 1. Применение XML-анализатора

    Вначале рассмотрим более слабый способ " Convert HTML Table to Array/Dataset " из "http://forums.asp.net/t/947241.aspx", который требует предварительной подготовки исходной таблицы.

  • В Solution Explorer вызовите контекстное меню для корня проекта ConvertTables и командой Add/Class добавьте класс с именем Convert1
  • Заполните класс Convert1 следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
    using System.Xml;
        
    namespace ConvertTables
    {
        class Convert1 // Source: "http://forums.asp.net/t/947241.aspx"
        {
            public void HtmlToDataTable()
            {
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(@"
                    <table border='1' cellpadding='0' cellspacing='0'> 
                      <tr>
                        <td width='50%'>cell 1a</td>
                        <td width='50%'>cell 1b</td>
                      </tr>
                      <tr>
                        <td width='50%'>cell 2a</td>
                        <td width='50%'>cell 2b</td>
                      </tr>
                    </table>
                            ");
        
                DataTable dt = new DataTable();
                dt.Columns.Add("Col1");
                dt.Columns.Add("Col2");
        
                foreach (XmlNode ndRow in doc.DocumentElement.ChildNodes)
                {
                    DataRow dr = dt.NewRow();
                    for (int colIndex = 0; colIndex < ndRow.ChildNodes.Count; colIndex++)
                        dr[colIndex] = ndRow.ChildNodes[colIndex].InnerText;
                    dt.Rows.Add(dr);
                }
        
                // Выводим на консоль
                foreach (DataRow row in dt.Rows)
                    Console.WriteLine("{0}    {1}", row["Col1"], row["Col2"]);
            }
        }
    }
  • Добавьте в файл Program.cs с функцией Main() код выполнения класса Convert1
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    namespace ConvertTables
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
        
                Convert1 convert1 = new Convert1();
                convert1.HtmlToDataTable();
        
                Console.ReadLine(); // Для задержки окна
            }
        }
    }
  • Запустите проект - должен получиться следующий результат
  • cell 1a            cell 1b
    cell 2a            cell 2b

    Пример 2. Применение регулярных выражений

    Применим код " Convert HTML tables to a DataSet " из "http://www.dotnetfunda.com/articles/article51.aspx". Этот подход более универсальный и может применяться как самостоятельно, так и в комбинации с первым способом. Вначале приведем код вместе с тестом, а затем приспособим его к своей задаче.

  • В Solution Explorer вызовите контекстное меню для корня проекта ConvertTables и командой Add/Class добавьте класс с именем Convert2
  • Наполните класс Convert2 следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
    using System.Xml;
    using System.Text.RegularExpressions;
        
    namespace ConvertTables
    {
        class Convert2 // Source: "http://www.dotnetfunda.com/articles/article51.aspx"
        {
            public String GetHTML()
            {
                StringBuilder sb = new StringBuilder();
        
                sb.AppendLine("<!DOCTYPE HTML PUBLIC " +
                    "-//W3C//DTD HTML 4.01 Transitional//EN" +
                    "http://www.w3.org/TR/html4/loose.dtd" +
                    ">");
                sb.AppendLine("<html>");
                sb.AppendLine("<head>");
                sb.AppendLine("<meta http-equiv=" +
                    "Content-Type" +
                    " content=" +
                    "text/html; charset=iso-8859-1" +
                    "> ");
        
                // Первая таблица (с заголовком)
                sb.AppendLine("<title>Title</title>");
                sb.AppendLine("</head>");
                sb.AppendLine("<body>");
                sb.AppendLine("<table>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<th>Table 1 - Header 1</th>");
                sb.AppendLine("<th>Table 1 - Header 2</th>");
                sb.AppendLine("<th>Table 1 - Header 3</th>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 1 - Row 1 - Column 1</td>");
                sb.AppendLine("<td>Table 1 - Row 1 - Column 2</td>");
                sb.AppendLine("<td>Table 1 - Row 1 - Column 3</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 1 - Row 2 - Column 1</td>");
                sb.AppendLine("<td>Table 1 - Row 2 - Column 2</td>");
                sb.AppendLine("<td>Table 1 - Row 2 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 1 - Row 3 - Column 1</td>");
                sb.AppendLine("<td>Table 1 - Row 3 - Column 2</td>");
                sb.AppendLine("<td>Table 1 - Row 3 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("</table>");
        
                // Вторая таблица (без заголовка)
                sb.AppendLine("<table>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 2 - Row 1 - Column 1</td>");
                sb.AppendLine("<td>Table 2 - Row 1 - Column 2</td>");
                sb.AppendLine("<td>Table 2 - Row 1 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 2 - Row 2 - Column 1</td>");
                sb.AppendLine("<td>Table 2 - Row 2 - Column 2</td>");
                sb.AppendLine("<td>Table 2 - Row 2 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 2 - Row 3 - Column 1</td>");
                sb.AppendLine("<td>Table 2 - Row 3 - Column 2</td>");
                sb.AppendLine("<td>Table 2 - Row 3 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("</table>");
        
                sb.AppendLine("</body>");
                sb.AppendLine("</html>");
        
                return sb.ToString();
            }
        
            public DataSet ConvertHTMLTablesToDataSet(String HTML)
            {
                // Локальные сылки
                DataSet ds = new DataSet();
                DataTable dt;
                DataRow dr;
                // DataColumn dc;
        
                bool HeadersExist = false;  // Флаг заголовка таблицы
                int iCurrentColumn = 0;     // Счетчик столбцов
                int iCurrentRow = 0;        // Счетчик строк
        
                // Регулярные выражения
                String TableExpression = "<table[^>]*>(.*?)</table>";
                String HeaderExpression = "<th[^>]*>(.*?)</th>";
                String RowExpression = "<tr[^>]*>(.*?)</tr>";
                String ColumnExpression = "<td[^>]*>(.*?)</td>";
                RegexOptions options = RegexOptions.Multiline |
                                       RegexOptions.Singleline |
                                       RegexOptions.IgnoreCase;
        
                // Заполняем коллекцию соответствий для всех таблиц HTML
                MatchCollection Tables = Regex.Matches(HTML, TableExpression, options);
        
                // Проходим по всем таблицам 
                foreach (Match Table in Tables)
                {
                    // Сбрасываем счетчик строки и флаг заголовка
                    HeadersExist = false;
                    iCurrentRow = 0;
        
                    // Создаем объект новой таблицы и наполняем его
                    dt = new DataTable();
        
                    /* Создаем соответствующее количество столбцов 
                     * для текущей таблицы (используем заголовки, 
                     * если они существуют, иначе используем 
                     * заданные по умолчанию названия)                
                     */
                    if (Table.Value.Contains("<th"))
                    {
                        HeadersExist = true;// Таблица содержит заголовок
                        // Заполняем коллекцию соответствий для всех <th> в таблице
                        MatchCollection Headers = Regex.Matches(
                            Table.Value, HeaderExpression, options);
        
                        // Добавляем столбцы с именами из строки заголовков текущей таблицы
                        foreach (Match Header in Headers)
                        {
                            dt.Columns.Add(Header.Groups[1].ToString());
                        }
                    }
                    else
                    {
                        Match tab = Regex.Matches(Table.Value, TableExpression, options)[0];
                        Match row = Regex.Matches(tab.ToString(), RowExpression, options)[0];
                        int count = Regex.Matches(row.ToString(), ColumnExpression, options).Count;
        
                        // Строки заголовков нет, добавляем дежурные имена
                        for (int iColumns = 0; iColumns < count; iColumns++)
                            dt.Columns.Add("Column" + iColumns);
                    }
        
                    // Извлекаем коллекцию строк текущей таблицы
                    MatchCollection Rows = Regex.Matches(Table.Value, RowExpression, options);
        
                    // Добавляем строки в объект DataRow
                    foreach (Match Row in Rows)
                    {
                        // Игнорируем строку заголовка
                        if (iCurrentRow != 0 || HeadersExist != true)
                        {
                            dr = dt.NewRow();
                            iCurrentColumn = 0;
                            // Извлекаем коллекцию столбцов текущей строки
                            MatchCollection Columns = Regex.Matches(Row.Value, ColumnExpression, options);
                            foreach (Match Column in Columns)
                            {
                                dr[iCurrentColumn] = Column.Groups[1].ToString();
                                iCurrentColumn++;
                            }
        
                            dt.Rows.Add(dr);
                        }
        
                        iCurrentRow++;
                    }
        
                    ds.Tables.Add(dt);
                }
        
                return ds;
            }
        }
    }
  • Удалите в файле Program.cs код выполнения класса Convert1 и добавьте код выполнения класса Convert2. В результате должно получиться следующее
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
        
    namespace ConvertTables
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
        
                Convert2 convert2 = new Convert2();
        
                String source = convert2.GetHTML();
                DataSet dataSet = convert2.ConvertHTMLTablesToDataSet(source);
        
                for (int i = 0; i < dataSet.Tables.Count; i++)
                {
                    // Распечатываем заголовки столбцов
                    foreach (DataColumn col in dataSet.Tables[i].Columns)
                        Console.Write("{0}    ", col.ColumnName);
                    Console.WriteLine();
        
                    // Распечатываем строки
                    foreach (DataRow row in dataSet.Tables[i].Rows)
                    {
                        foreach (DataColumn col in dataSet.Tables[i].Columns)
                            Console.Write("{0}    ", row[col].ToString());
                        Console.WriteLine();
                    }
        
                    Console.WriteLine();
                }
        
                Console.ReadLine(); // Для задержки окна
            }
        }
    }
  • Запустите проект - должен получиться следующий результат
  • Table 1 - Header 1     Table 1 - Header 2     Table 1 - Header 3
    Table 1 - Row 1 - Column 1     Table 1 - Row 1 - Column 2     Table 1 - Row 1 - Column 3
    Table 1 - Row 2 - Column 1     Table 1 - Row 2 - Column 2     Table 1 - Row 2 - Column 3
    Table 1 - Row 3 - Column 1     Table 1 - Row 3 - Column 2     Table 1 - Row 3 - Column 3
    
    Column0     Column1     Column2
    Table 2 - Row 1 - Column 1     Table 2 - Row 1 - Column 2     Table 2 - Row 1 - Column 3
    Table 2 - Row 2 - Column 1     Table 2 - Row 2 - Column 2     Table 2 - Row 2 - Column 3
    Table 2 - Row 3 - Column 1     Table 2 - Row 3 - Column 2     Table 2 - Row 3 - Column 3

    Пример 3. Преобразование группы таблиц с текстами в наборы данных DataSet

    Воспользуемся вторым способом извлечения информации из HTML -таблиц в DataSet и немного изменим задачу. Пусть у нас имеются HTML -файлы исключительно с таблицами текстов одинаковой структуры, например, пусть одна из таблиц будет такой

    QueryID Query CorrectAnswer Comment Theme
    1_4 Каким транспортным средствам в обозначенной знаком зоне запрещена стоянка в выходные и праздничные дни ? 1. Только грузовым автомобилям с разрешенной максимальной массой более 3,5 т. 2. Всем грузовым автомобилям. 3. Всем транспортным средствам. 1 Знак 5.27 "Зона с ограничением стоянки" относится к зональным знакам, требования которых действуют на всей территории (участке дороги), обозначенной такими знаками, вплоть до выезда из зоны, обозначенного в данном случае знаком 5.28 "Конец зоны с ограничением стоянки". Табличка 8.5.1, размещенная под знаком, распространяет его действие только на субботние, воскресные и праздничные дни. А изображение таблички 8.4.1 "Вид транспортного средства" в нижней части знака информирует о том, что в обозначенной зоне стоянка запрещена только грузовым автомобилям, у которых разрешенная максимальная масса превышает 3,5 т. 4
    1_9 По какой траектории Вы имеете право выполнить разворот, управляя автопоездом, имеющим большую длину ? 1. Только по А. 2. Только по Б. 3. По любой. 1 При недостаточной для разворота ширине проезжей части Правила разрешают вне перекрестка выполнение маневра не из крайнего левого положения, а с обочины или от правого края проезжей части (п. 8.8), т.е. по траектории А. 8
    1_10 С какой скоростью Вы можете продолжить движение вне населенного пункта по левой полосе на грузовом автомобиле с разрешенной максимальной массой более 3,5 т ? 1. Не более 50 км/ч. 2. Не менее 50 км/ч и не более 90 км/ч. 3. Не менее 50 км/ч и не более 70 км/ч. 3 Знак 4.6 "Ограничение минимальной скорости" и табличка 8.14 "Полоса движения" предписывают двигаться по левой полосе со скоростью не менее 50 км/ч. Однако при этом на дороге вне населенного пункта, не относящейся к автомагистрали, вы не имеете права развивать скорость на грузовом автомобиле более 70 км/ч (п. 10.3). 10

    Для правильной работы утилиты таблица должна удовлетворять следующим условиям:

  • HTML -файл должен содержать хотя бы одну таблицу
  • Все ячейки таблицы должны содержать параграфы, включая каждый ответ
  • Заголовки таблицы будут являться заголовками столбцов в DataTable
  • Необходимо удалить управляющие символы quot; (кавычки) и nbsp; (пробел) из всей таблицы, иначе они перейдут в DataSet
  • Наша задача, разработать утилиту, которая бы заполнила ADO.NET -объект DataTable или DataSet содержимым одной таблицы <table> или группы таблиц. Утилита будет способна обрабатывать много таблиц, но применим ее только к одной таблице Ticket1.htm, которая приведена в каталоге Source. При желании, можно развить эту утилиту до функциональности, чтобы она создавала и заполняла данными, извлеченными из <table>, таблицу (или несколько таблиц) базы данных.

    В процессе преобразования исходной таблицы HTML будем заполнять наборы данных для вопросов и отдельно для ответов, чтобы получить следующие представления (на примере одной исходной таблицы)

    QueryID Query CorrectAnswer Comment Theme
    4 Каким транспортным средствам в обозначенной знаком зоне запрещена стоянка в выходные и праздничные дни ? 1 Знак 5.27 "Зона с ограничением стоянки" относится к зональным знакам, требования которых действуют на всей территории (участке дороги), обозначенной такими знаками, вплоть до выезда из зоны, обозначенного в данном случае знаком 5.28 "Конец зоны с ограничением стоянки". Табличка 8.5.1, размещенная под знаком, распространяет его действие только на субботние, воскресные и праздничные дни. А изображение таблички 8.4.1 "Вид транспортного средства" в нижней части знака информирует о том, что в обозначенной зоне стоянка запрещена только грузовым автомобилям, у которых разрешенная максимальная масса превышает 3,5 т. 4
    9 По какой траектории Вы имеете право выполнить разворот, управляя автопоездом, имеющим большую длину ? 1 При недостаточной для разворота ширине проезжей части Правила разрешают вне перекрестка выполнение маневра не из крайнего левого положения, а с обочины или от правого края проезжей части (п. 8.8), т.е. по траектории А. 8
    10 С какой скоростью Вы можете продолжить движение вне населенного пункта по левой полосе на грузовом автомобиле с разрешенной максимальной массой более 3,5 т ? 3 Знак 4.6 "Ограничение минимальной скорости" и табличка 8.14 "Полоса движения" предписывают двигаться по левой полосе со скоростью не менее 50 км/ч. Однако при этом на дороге вне населенного пункта, не относящейся к автомагистрали, вы не имеете права развивать скорость на грузовом автомобиле более 70 км/ч (п. 10.3). 10

    QueryID AnswerID Answer
    4 1 Только грузовым автомобилям с разрешенной максимальной массой более 3,5 т.
    4 2 Всем грузовым автомобилям.
    4 3 Всем транспортным средствам.
    9 1 Только по А.
    9 2 Только по Б.
    9 3 По любой.
    10 1 Не более 50 км/ч.
    10 2 Не менее 50 км/ч и не более 90 км/ч.
    10 3 Не менее 50 км/ч и не более 70 км/ч.
  • Добавьте к решению новый проект консольного приложения с именем CreateDataSet и назначьте его стартовым в решении
  • Добавьте к проекту новый класс с именем CreateData в файле CreateData.cs и заполните его так
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
    using System.Text.RegularExpressions;
        
    namespace CreateDataSet
    {
        public enum SelectTicket
        {
            AB,
            CD,
            ABCD
        }
        
        class CreateData
        {
            // Source: "http://www.dotnetfunda.com/articles/article51.aspx"
            void ConvertHTMLTablesToDataSet(
                String HTML, DataSet dsQuestions, DataSet dsAnswers)
            {
                // Локальные сылки
                DataTable dtQuestions;
                DataRow drQuestions;
                DataTable dtAnswers;
                DataRow drAnswers;
        
                bool HeadersExist = false;  // Флаг существования заголовка таблицы
                int iCurrentColumn = 0;     // Счетчик столбцов
                int iCurrentRow = 0;        // Счетчик строк
        
                // Регулярные выражения и опции
                String TableExpression = "<table[^>]*>(.*?)</table>";
                String HeaderExpression = "<th[^>]*>(.*?)</th>";
                String RowExpression = "<tr[^>]*>(.*?)</tr>";
                String ColumnExpression = "<td[^>]*>(.*?)</td>";
                String ParagraphExpression = "<p[^>]*>(.*?)</p>";
                RegexOptions options = RegexOptions.Multiline |
                                       RegexOptions.Singleline |
                                       RegexOptions.IgnoreCase;
        
                // Извлекаем коллекцию таблиц из содержимого HTML
                MatchCollection Tables = Regex.Matches(HTML, TableExpression, options);
        
                // Проходим по всем HTML-таблицам 
                foreach (Match Table in Tables)
                {
                    // Сбрасываем счетчик строки и флаг заголовка
                    HeadersExist = false;
                    iCurrentRow = 0;
        
                    // Создаем объект новой таблицы и наполняем его
                    dtQuestions = new DataTable();
                    dtAnswers = new DataTable();
        
                    /* Создаем соответствующее количество столбцов 
                     * для текущей таблицы (используем заголовки, 
                     * если они существуют, иначе используем 
                     * заданные по умолчанию названия)                
                     */
                    if (Table.Value.Contains("<th"))
                    {
                        HeadersExist = true;// Таблица содержит заголовок
                        // Заполняем коллекцию соответствий для всех <th> в таблице
                        MatchCollection Headers = Regex.Matches(
                            Table.Value, HeaderExpression, options);
        
                        // Добавляем столбцы с именами из строки заголовков текущей таблицы
                        foreach (Match Header in Headers)
                        {
                            // Groups[0] - с дескрипторами <p>Содержимое</p>
                            // Groups[1] - только Содержимое
                            MatchCollection paragraphs = Regex.Matches(Header.Value, ParagraphExpression, options);
                            if (paragraphs.Count != 0)
                                dtQuestions.Columns.Add(paragraphs[0].Groups[1].ToString());
                            else
                                dtQuestions.Columns.Add(Header.Groups[1].ToString());
                        }
                    }
                    else
                    {
                        Match tab = Regex.Matches(Table.Value, TableExpression, options)[0];
                        Match row = Regex.Matches(tab.ToString(), RowExpression, options)[0];
                        int count = Regex.Matches(row.ToString(), ColumnExpression, options).Count;
        
                        // Строки заголовков нет, добавляем дежурные имена
                        for (int iColumns = 0; iColumns < count; iColumns++)
                            dtQuestions.Columns.Add("Column" + iColumns);
                    }
        
                    String[] TableAnswerColumnsName = { "QueryID", "AnswerID", "Answer" };
                    for (int iColumns = 0; iColumns < TableAnswerColumnsName.Length; iColumns++)
                        dtAnswers.Columns.Add(TableAnswerColumnsName[iColumns]);
        
                    // Извлекаем коллекцию строк текущей таблицы
                    MatchCollection Rows = Regex.Matches(Table.Value, RowExpression, options);
        
                    // Добавляем строки в объект DataRow
                    foreach (Match Row in Rows)
                    {
                        // Игнорируем строку заголовка
                        if (iCurrentRow != 0 || HeadersExist != true)
                        {
                            drQuestions = dtQuestions.NewRow();
                            iCurrentColumn = 0;
                            // Извлекаем коллекцию столбцов текущей строки
                            MatchCollection Columns = Regex.Matches(Row.Value, ColumnExpression, options);
                            foreach (Match Column in Columns)
                            {
                                // Извлекаем коллекцию параграфов из текущего столбца
                                MatchCollection paragraphs = Regex.Matches(Column.Value, ParagraphExpression, options);
                                if (paragraphs.Count != 0)
                                {
                                    int iAnswer = 0;
                                    String line = String.Empty;
                                    foreach (Match paragraph in paragraphs)
                                    {
                                        line = paragraph.Groups[1].ToString();
        
                                        // Убираем все лишнее
                                        string[] split = line.Split(new Char[] { '\r', '\n', ' ' });
                                        StringBuilder strBuilder = new StringBuilder();
                                        foreach (string s in split)
                                        {
                                            string word = s.Trim();
                                            if (word != String.Empty)
                                                strBuilder.Append(" " + word);
                                        }
                                        line = strBuilder.ToString().Trim();
        
                                        // Убираем номера ответов
                                        int pos = line.IndexOf(".");
                                        if (pos > 0  pos <= 2  iCurrentColumn == 1)
                                        {
                                            // Ответы 
                                            drAnswers = dtAnswers.NewRow();
                                            drAnswers[0] = drQuestions[0];
                                            iAnswer++;
                                            drAnswers[1] = iAnswer.ToString();
                                            line = line.Substring(pos + 1).Trim();
                                            drAnswers[2] = line;
                                            dtAnswers.Rows.Add(drAnswers);
                                            continue;
                                        }
        
                                        if (iAnswer == 0)
                                            drQuestions[iCurrentColumn] = line;
                                    }
                                }
                                else
                                    drQuestions[iCurrentColumn] = Column.Groups[1].ToString();
        
                                iCurrentColumn++;
                            }
        
                            dtQuestions.Rows.Add(drQuestions);
                        }
        
                        iCurrentRow++;
                    }
        
                    dsQuestions.Tables.Add(dtQuestions);
                    dsAnswers.Tables.Add(dtAnswers);
                }
            }
        
            void FillDataset(string path, DataSet dsQuestions, DataSet dsAnswers, int count)
            {
                for (int i = 1; i <= count; i++)
                {
                    string file = path + "\\Ticket" + i.ToString() + ".htm";
                    System.IO.StreamReader streamReader = new System.IO.StreamReader(
                        file, Encoding.GetEncoding(1251));
        
                    String source = streamReader.ReadToEnd();
        
                    DataSet dsQuestionsItem = new DataSet();
                    DataSet dsAnswersItem = new DataSet();
                    ConvertHTMLTablesToDataSet(source, dsQuestionsItem, dsAnswersItem);
        
                    foreach (DataTable table in dsQuestionsItem.Tables)
                    {
                        DataRow[] rows = new DataRow[table.Rows.Count];
                        for (int iRow = 0; iRow < rows.Length; iRow++)
                        {
                            rows[iRow] = table.Rows[iRow];
                            rows[iRow][0] = (rows[iRow][0]).ToString().Trim();
                        }
                        dsQuestions.Merge(rows);
                    }
                    foreach (DataTable table in dsAnswersItem.Tables)
                    {
                        DataRow[] rows = new DataRow[table.Rows.Count];
                        for (int iRow = 0; iRow < rows.Length; iRow++)
                        {
                            rows[iRow] = table.Rows[iRow];
                            rows[iRow][0] = (rows[iRow][0]).ToString().Trim();
                        }
                        dsAnswers.Merge(rows);
                    }
                }
            }
        
            // Поля
            DataSet dsQuestionsAB = new DataSet();
            DataSet dsAnswersAB = new DataSet();
            DataSet dsQuestionsCD = new DataSet();
            DataSet dsAnswersCD = new DataSet();
            int lenQuery = 0, lenComment = 0, lenAnswer = 0;
            int countTicket = 40; // Количество файлов-билетов
        
            // Свойства
            public DataSet QuestionsAB { get { return dsQuestionsAB; } }
            public DataSet AnswersAB { get { return dsAnswersAB; } }
            public DataSet QuestionsCD { get { return dsQuestionsCD; } }
            public DataSet AnswersCD { get { return dsAnswersCD; } }
            public int LenQuery { get { return lenQuery; } }
            public int LenComment { get { return lenComment; } }
            public int LenAnswer { get { return lenAnswer; } }
            public int CountTicket { set { countTicket = value; } }
        
            public void Execute(SelectTicket selectTicket)
            {
                string path = System.IO.Directory.GetCurrentDirectory();
        
                int len;
                switch (selectTicket)
                {
                    case SelectTicket.AB:
                        // Заполняем DataSet
                        FillDataset(path + @"\AB", dsQuestionsAB, dsAnswersAB, countTicket);
        
                        // Считаем размеры полей для создания объектных таблиц
                        foreach (DataRow row in dsQuestionsAB.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersAB.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        break;
                    case SelectTicket.CD:
                        // Заполняем DataSet
                        FillDataset(path + @"\CD", dsQuestionsCD, dsAnswersCD, countTicket);
        
                        // Считаем размеры полей для создания объектных таблиц
                        foreach (DataRow row in dsQuestionsCD.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersCD.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        break;
                    case SelectTicket.ABCD:
                        // Заполняем DataSet
                        FillDataset(path + @"\AB", dsQuestionsAB, dsAnswersAB, countTicket);
                        FillDataset(path + @"\CD", dsQuestionsCD, dsAnswersCD, countTicket);
        
                        // Считаем размеры полей для создания объектных таблиц
                        foreach (DataRow row in dsQuestionsAB.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersAB.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        foreach (DataRow row in dsQuestionsCD.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersCD.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        break;
                }
            }
        
            public void Show(DataSet dsQuestions, DataSet dsAnswers)
            {
                for (int i = 0; i < dsQuestions.Tables.Count; i++)
                {
                    // Распечатываем заголовки столбцов
                    foreach (DataColumn col in dsQuestions.Tables[i].Columns)
                        Console.WriteLine("{0}", col.ColumnName);
                    Console.WriteLine();
        
                    // Распечатываем строки
                    foreach (DataRow row in dsQuestions.Tables[i].Rows)
                    {
                        foreach (DataColumn col in dsQuestions.Tables[i].Columns)
                            Console.WriteLine("{0}", row[col].ToString());
                        Console.WriteLine();
                    }
        
                    Console.WriteLine();
                }
        
                for (int i = 0; i < dsAnswers.Tables.Count; i++)
                {
                    // Распечатываем заголовки столбцов
                    foreach (DataColumn col in dsAnswers.Tables[i].Columns)
                        Console.WriteLine("{0}", col.ColumnName);
                    Console.WriteLine();
        
                    // Распечатываем строки
                    foreach (DataRow row in dsAnswers.Tables[i].Rows)
                    {
                        foreach (DataColumn col in dsAnswers.Tables[i].Columns)
                            Console.WriteLine("{0}", row[col].ToString());
                        Console.WriteLine();
                    }
        
                    Console.WriteLine();
                }
            }
        }
    }

    При создании экземпляра этого класса в клиенте должен быть произведен разбор кода HTML -таблиц и заполнены 4 (если режим SelectTicket.ABCD ) набора данных типа DataSet, ссылки на которые доступны через свойства QuestionsAB, AnswersAB, QuestionsCD, AnswersCD. Попутно будут вычислены максимальные размеры полей LenQuery, LenComment, LenAnswer.

  • Модифицируйте файл Program.cs следующим образом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
        
    namespace CreateDataSet
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.WindowWidth = 81;
        
                CreateData tables = new CreateData();
                tables.CountTicket = 1;
                tables.Execute(SelectTicket.CD);
                tables.Show(tables.QuestionsCD, tables.AnswersCD);
        
                Console.WriteLine(String.Format("LenQuery={0}; LenComment={1}; LenAnswer={2}",
                    tables.LenQuery, tables.LenComment, tables.LenAnswer));
        
                Console.ReadLine();
            }
        }
    }
  • Через панель Solution Explorer создайте в корне текущего проекта папку CD и скопируйте в нее контекстной командой Add/Existing Item из прилагаемого к работе каталога Source файл Ticket1.htm
  • В Solution Explorer выделите файл Ticket1.htm и в панели Properties назначьте ему свойство
  • Build Action=Content
  • Copy to Output Directory=Copy if newer
  • Запустите проект на выполнение - получится следующий консольный вывод
  • QueryID
    Query
    CorrectAnswer
    Comment
    Theme 
    1_4
    Каким транспортным средствам в обозначенной знаком зоне запрещена стоянка в выходные и праздничные дни ?
    1
    Знак 5.27 "Зона с ограничением стоянки" относится к зональным знакам, требования которых действуют 
    на всей территории (участке дороги), обозначенной такими знаками, вплоть до выезда из зоны, обозначенного в данном случае 
    знаком 5.28 "Конец зоны с ограничением стоянки". Табличка 8.5.1, размещенная под знаком, распространяет его 
    действие только на субботние, воскресные и праздничные дни. А изображение таблички 8.4.1 "Вид транспортного средства" 
    в нижней части знака информирует о том, что в обозначенной зоне стоянка запрещена только грузовым автомобилям,
    у которых разрешенная максимальная масса превышает 3,5 т.
    4
    1_9
    По какой траектории Вы имеете право выполнить разворот, управляя автопоездом, имеющим большую длину ?
    1
    При недостаточной для разворота ширине проезжей части Правила разрешают вне перекрестка выполнение маневра не из крайнего 
    левого положения, а с обочины или от правого края проезжей части (п. 8.8), т.е. по траектории А.
    8
    1_10
    С какой скоростью Вы можете продолжить движение вне населенного пункта по левой полосе на грузовом автомобиле с 
    С разрешенной максимальной массой более 3,5 т ?
    3
    Знак 4.6 "Ограничение минимальной скорости" и табличка 8.14 "Полоса движения" предписывают двигаться по 
    левой полосе со скоростью не менее 50 км/ч. Однако при этом на дороге вне населенного пункта, не относящейся к автомагистрали, 
    вы не имеете права развивать скорость на грузовом автомобиле более 70 км/ч (п. 10.3).
    10
    QueryID
    AnswerID
    Answer
    1_4
    1
    Только грузовым автомобилям с разрешенной максимальной массой более 3,5 т.
    1_4
    2
    Всем грузовым автомобилям.
    1_4
    3
    Всем транспортным средствам.
    1_9
    1
    Только по А.
    1_9
    2
    Только по Б.
    1_9
    3
    По любой.
    1_10
    1
    Не более 50 км/ч.
    1_10
    2
    Не менее 50 км/ч и не более 90 км/ч.
    1_10
    3
    Не менее 50 км/ч и не более 70 км/ч.
    LenQuery=157; LenComment=615; LenAnswer=74

    Мы видим, что данные извлекаются утилитой из HTML -таблицы правильно и размещаются построчно в соответствующих полях объекта DataSet.

    Упражнение 3. Изменение ширины окна колесом мыши

    В заключение приведем интересный способ регулирования ширины окна WPF, приведенный в http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/a4b5150f-5d70-4d33-9355-4cac9521677d

  • Создайте приложение WPF Application с именем WpfApplication1, внесите в него приведенный ниже код и испытайте работу
  • <Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        Name="window1"  MouseWheel="Window_MouseWheel">
        <Grid>
        </Grid>
    </Window>
    using System;
    using System.Windows;
    using System.Windows.Input;
    using System.Windows.Media.Animation; 
        
    namespace WpfApplication1
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
            {
                int delta = e.Delta;
                double w1 = this.window1.ActualWidth;
                double w2 = w1 + 2 * delta / 10;
                DoubleAnimation anima = new DoubleAnimation();
                anima.Duration = new Duration(TimeSpan.FromSeconds(0.3));
                anima.From = w1;
                anima.To = w2;
                anima.FillBehavior = FillBehavior.HoldEnd;
                this.window1.BeginAnimation(Window.WidthProperty, anima);
            }
        }
    }
  • Внимательно разберитесь со всем приведенным кодом упражнений. Такой подход - самый короткий путь научиться программировать
  • Страницы:

    Упражнение 1. Создание компонента WPF "Цифровой таймер"

    Все необходимые для выполнения данной работы программы можно найти в Setup.zip.

    В WPF предусмотрены два вида пользовательских элементов:

  • Пользовательские элементы управления - в Visual Studio это тип проекта WPF User Control Library
  • Настраиваемые пользовательские элементы - в Visual Studio это тип проекта WPF Custom Control Library
  • Настраиваемые пользовательские элементы невидимы. Для них проектная плоскость Design с возможностью перетаскивания элементов из панели Toolbox не предусмотрена, поэтому надо создавать шаблон. А пользовательские элементы управления имеют проектную плоскость времени проектирования и их можно создавать точно также, как проект интерфейса приложения WPF Application. Но запускать на выполнение пользовательские элементы управления можно только с помощью исполнимого теста.

    Поставим задачу разработать секундомер, показывающий время, оставшееся до наступления какого-нибудь события. Объект таймера оформим как пользовательский элемент управления, помещенный в библиотеку типа WPF User Control Library. При построении используем механизмы ресурсов и привязки WPF.

    Создание таймера в библиотеке

  • Создайте решение ClockAndConvertHTML и новый проект библиотеки WpfControlLibrary, используя шаблон WPF User Control Library в типе проектов Visual Studio
  • В Solution Explorer переименуйте автоматически сгенерированный файл UserControl1.xaml в Clock.xaml
  • В редакторе файла Clock.xaml на вкладке XAML жестом Ctrl+H вызовите окно замены с настройкой Current Project и замените кнопкой Replace All все вхождения UserControl1 на Clock
  • В панели Solution Explorer вызовите контекстное меню для корневого узла WpfControlLibrary проекта и добавьте папку Code командой Add/New Folder
  • В панели Solution Explorer вызовите контекстное меню для папки Code и командой Add/Class добавьте в нее файл с именем DigitalClock.cs, который заполните так
  • using System;
    using System.Windows;           // DependencyObject, DependencyProperty
    using System.ComponentModel;    // INotifyPropertyChanged
    using System.Windows.Threading; // DispatcherTimer
    using System.Globalization;     // CultureInfo 
    using System.Windows.Controls;  // TextBlock
    using System.Windows.Media;     // FontFamily
        
    namespace MyClock
    {
        public class DigitalClock : TextBlock, INotifyPropertyChanged
        {
            const int COUNT_TIME = 10;    // Минуты
        
            // Событие реализации интерфейса INotifyPropertyChanged
            public event PropertyChangedEventHandler PropertyChanged;
        
            // Обычное событие остановки таймера
            public event EventHandler StopDigitalClock;
        
            // Открытое свойство 
            int minutes, seconds, time;
            public TextBlock Time
            {
                get
                {
                    minutes = time / 60;
                    seconds = time % 60;
                    this.Text = String.Format("{0:00}:{1:D2}", minutes, seconds);
                    if (time > 0)
                        time -= 1;
                    return this;
                }
            }
        
            // Конструктор запускает таймер
            DispatcherTimer timer;
            public DigitalClock()
            {
                // Настраиваем дизайн
                this.FontFamily = new FontFamily("Arial");
                this.FontSize = 1.3333 * 36;    // 36pt
                this.FontWeight = FontWeights.Bold;
                this.Background = Brushes.Red;
                this.Foreground = Brushes.Yellow;
                this.Padding = new Thickness(5, 0, 5, 0);
        
                // Создаем таймер
                timer = new DispatcherTimer();
                timer.Tick += new EventHandler(timer_Tick);
                timer.Interval = TimeSpan.FromSeconds(1);
        
                // Начальное значение
                time = COUNT_TIME * 60;   // Секунды
            }
        
            public void Start()
            {
                timer.Stop();
                timer.Start();
            }
        
            public void Restart()
            {
                time = COUNT_TIME * 60;   // Секунды
                timer.Stop();
                timer.Start();
            }
        
            public void StopResult()
            {
                timer.Stop();
                if (StopDigitalClock != null)
                    StopDigitalClock(this, EventArgs.Empty);
            }
        
            public void Stop()
            {
                timer.Stop();
            }
        
            // Обработчик события таймера возбуждает свойство Time
            // и генерирует обычное событие останова 
            void timer_Tick(object sender, EventArgs e)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Time"));
        
                if (minutes <= 0  seconds <= 0)
                {
                    timer.Stop();
                    if (StopDigitalClock != null)
                        StopDigitalClock(this, EventArgs.Empty);
                }
            }
        }
    }

    Класс возбуждает обычное событие StopDigitalClock, когда заканчивается лимит времени.

  • Откройте на редактирование файл Clock.xaml в представлении XAML и заполните его следующей разметкой
  • <UserControl x:Class="WpfControlLibrary.Clock"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyClock"
        >
        <UserControl.Resources>
            <local:DigitalClock x:Key="clock" StopDigitalClock="DigitalClock_StopDigitalClock" />
        </UserControl.Resources>
        <Label Content="{Binding Source={StaticResource clock}, Path=Time}"
               BorderBrush="Blue" BorderThickness="5" Padding="0"
               HorizontalAlignment="Center" />
    </UserControl>
  • Откройте на редактирование файл Clock.xaml.cs и заполните его следующим процедурным кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    using MyClock; // Пространство имен таймера в библиотеке WpfControlLibrary
        
    namespace WpfControlLibrary
    {
        public partial class Clock : UserControl
        {
            public Clock()
            {
                InitializeComponent();
        
                // Извлекаем объект из коллекции ресурсов
                clock = this.Resources["clock"] as DigitalClock;
            }
            DigitalClock clock; // Ссылка на экземпляр из ресурсов
        
            // Поле маршрутизированного события
            public static readonly RoutedEvent StopClockEvent;
        
            // Статический конструктор
            static Clock()
            {
                // Регистрация маршрутизированного события
                StopClockEvent = EventManager.RegisterRoutedEvent(
                    "StopClock", RoutingStrategy.Bubble,
                    typeof(RoutedEventHandler), typeof(Clock));
            }
        
            // Контейнер для подписки на маршрутизированное событие
            public event RoutedEventHandler StopClock
            {
                add { AddHandler(StopClockEvent, value); }
                remove { RemoveHandler(StopClockEvent, value); }
            }
        
            // Возбуждение маршрутизированного события через обработчик простого события
            private void DigitalClock_StopDigitalClock(object sender, EventArgs e)
            {
                this.RaiseEvent(new RoutedEventArgs(Clock.StopClockEvent));
            }
        
            // Функции доступа для управления таймером из клиента
            public void Start()
            {
                clock.Start();
            }
            public void Restart()
            {
                clock.Restart();
            }
            public void StopResult()
            {
                clock.StopResult();
            }
            public void Stop()
            {
                clock.Stop();
            }
        }
    }

    Обычное событие StopDigitalClock через обработчик возбуждает всплывающее событие StopClock, которое в клиенте можно будет слушать и управлять интерфейсом. Для управления таймером в классе Clock предусмотрены функции доступа.

  • Выполните команду меню Build/Build Solution или жест F6, чтобы создать библиотечную сборку
  • В панели Solution Explorer щелкните на пиктограмме Show All Files и убедитесь, что сборка WpfControlLibrary.dll действительно создана
  • Создание тестового приложения

    Проверим работу созданного таймера с помощью тестирующего приложения.

  • Командой меню оболочки File/Add/New Project добавьте к решению исполнимую сборку типа WPF Application с именем TestClock
  • В панели Solution Explorer вызовите контекстное меню для узла TestClock и командой Set as StartUp Project назначьте его стартовым. Обратите внимание, что после выполнение этой команды узел стартового проекта выделился полужирным стилем
  • В Solution Explorer выполните для узла References проекта TestClock команду Add Reference контекстного меню и на вкладке Projects подключите библиотечную сборку WpfControlLibrary.dll к тестовому проекту
  • В Solution Explorer выделите любой узел тестового проекта TestClock, щелкните на пиктограмме Show All Files и раскройте узел bin\Debug - убедитесь, что пока он пустой
  • Откомпилируйте тестовый проект и щелкните в Solution Explorer на пиктограмме Refresh - убедитесь, что оболочка скопировала в каталог размещения тестовой сборки нашу библиотечную сборку WpfControlLibrary.dll
  • Откройте на редактирование файл Window1.xaml в представлении XAML и заполните его следующей разметкой
  • <Window x:Class="TestClock.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lib="clr-namespace:WpfControlLibrary;assembly=WpfControlLibrary"
        Title="TestClock" Height="300" Width="300"
        lib:Clock.StopClock="Window_StopClock" 
            >
        <StackPanel lib:Clock.StopClock="StackPanel_StopClock">
            <lib:Clock Name="timer" />
            <Button Click="start_Click">Start</Button>
            <Button Click="restart_Click">Restart</Button>
            <Button Click="stopResult_Click">StopResult</Button>
            <Button Click="stop_Click">Stop</Button>
        </StackPanel>
    </Window>
  • Заполните файл Window1.xaml.cs следующим процедурным кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace TestClock
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            private void start_Click(object sender, RoutedEventArgs e)
            {
                timer.Start();
            }
        
            private void restart_Click(object sender, RoutedEventArgs e)
            {
                timer.Restart();
            }
        
            private void stopResult_Click(object sender, RoutedEventArgs e)
            {
                timer.StopResult();
            }
        
            private void stop_Click(object sender, RoutedEventArgs e)
            {
                timer.Stop();
            }
        
            private void Window_StopClock(object sender, RoutedEventArgs e)
            {
                MessageBox.Show("StopClock", "Window1");
            }
        
            private void StackPanel_StopClock(object sender, RoutedEventArgs e)
            {
                MessageBox.Show("StopClock", "StackPanel");
            }
        }
    }

    На прослушивание всплывающего события StopClock мы в разметке подписали два интерфейсных элемента: Window и StackPanel, которые в своих обработчиках выдают диалоговое окно MessageBox. Это позволит проследить прохождение нашего события, чтобы убедиться, что оно действительно всплывает.

  • Запустите тестовый проект и испытайте работу таймера - функционирует как и задумали
  • Таким образом, мы спроектировали простой цифровой таймер и упаковали его в библиотеку. Для проверки работы таймера разработали тестирующую исполнимую сборку и подключили к ней библиотечную сборку. И все работает. Пример простой, но показательный. В дальнейшем, при развитии текущего решения или разработке новых проектов можно добавлять в ту же самую библиотеку и другие элементы управления пользовательского интерфейса.

    Упражнение 2. Разработка утилиты преобразования HTML-таблиц <table> в объекты DataSet

    Для начала используем программные тесты с кодом HTML. Рассмотрим два способа разбора XML -кода. Для реализации этих способов воспользуемся консольным приложением.

  • Командой меню File/Add/New Project добавьте к решению новый проект с именем ConvertTables и назначьте его стартовым
  • Оболочка автоматически создала файл Program.cs с точкой входа в приложение.

    Пример 1. Применение XML-анализатора

    Вначале рассмотрим более слабый способ " Convert HTML Table to Array/Dataset " из "http://forums.asp.net/t/947241.aspx", который требует предварительной подготовки исходной таблицы.

  • В Solution Explorer вызовите контекстное меню для корня проекта ConvertTables и командой Add/Class добавьте класс с именем Convert1
  • Заполните класс Convert1 следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
    using System.Xml;
        
    namespace ConvertTables
    {
        class Convert1 // Source: "http://forums.asp.net/t/947241.aspx"
        {
            public void HtmlToDataTable()
            {
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(@"
                    <table border='1' cellpadding='0' cellspacing='0'> 
                      <tr>
                        <td width='50%'>cell 1a</td>
                        <td width='50%'>cell 1b</td>
                      </tr>
                      <tr>
                        <td width='50%'>cell 2a</td>
                        <td width='50%'>cell 2b</td>
                      </tr>
                    </table>
                            ");
        
                DataTable dt = new DataTable();
                dt.Columns.Add("Col1");
                dt.Columns.Add("Col2");
        
                foreach (XmlNode ndRow in doc.DocumentElement.ChildNodes)
                {
                    DataRow dr = dt.NewRow();
                    for (int colIndex = 0; colIndex < ndRow.ChildNodes.Count; colIndex++)
                        dr[colIndex] = ndRow.ChildNodes[colIndex].InnerText;
                    dt.Rows.Add(dr);
                }
        
                // Выводим на консоль
                foreach (DataRow row in dt.Rows)
                    Console.WriteLine("{0}    {1}", row["Col1"], row["Col2"]);
            }
        }
    }
  • Добавьте в файл Program.cs с функцией Main() код выполнения класса Convert1
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    namespace ConvertTables
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
        
                Convert1 convert1 = new Convert1();
                convert1.HtmlToDataTable();
        
                Console.ReadLine(); // Для задержки окна
            }
        }
    }
  • Запустите проект - должен получиться следующий результат
  • cell 1a            cell 1b
    cell 2a            cell 2b

    Пример 2. Применение регулярных выражений

    Применим код " Convert HTML tables to a DataSet " из "http://www.dotnetfunda.com/articles/article51.aspx". Этот подход более универсальный и может применяться как самостоятельно, так и в комбинации с первым способом. Вначале приведем код вместе с тестом, а затем приспособим его к своей задаче.

  • В Solution Explorer вызовите контекстное меню для корня проекта ConvertTables и командой Add/Class добавьте класс с именем Convert2
  • Наполните класс Convert2 следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
    using System.Xml;
    using System.Text.RegularExpressions;
        
    namespace ConvertTables
    {
        class Convert2 // Source: "http://www.dotnetfunda.com/articles/article51.aspx"
        {
            public String GetHTML()
            {
                StringBuilder sb = new StringBuilder();
        
                sb.AppendLine("<!DOCTYPE HTML PUBLIC " +
                    "-//W3C//DTD HTML 4.01 Transitional//EN" +
                    "http://www.w3.org/TR/html4/loose.dtd" +
                    ">");
                sb.AppendLine("<html>");
                sb.AppendLine("<head>");
                sb.AppendLine("<meta http-equiv=" +
                    "Content-Type" +
                    " content=" +
                    "text/html; charset=iso-8859-1" +
                    "> ");
        
                // Первая таблица (с заголовком)
                sb.AppendLine("<title>Title</title>");
                sb.AppendLine("</head>");
                sb.AppendLine("<body>");
                sb.AppendLine("<table>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<th>Table 1 - Header 1</th>");
                sb.AppendLine("<th>Table 1 - Header 2</th>");
                sb.AppendLine("<th>Table 1 - Header 3</th>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 1 - Row 1 - Column 1</td>");
                sb.AppendLine("<td>Table 1 - Row 1 - Column 2</td>");
                sb.AppendLine("<td>Table 1 - Row 1 - Column 3</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 1 - Row 2 - Column 1</td>");
                sb.AppendLine("<td>Table 1 - Row 2 - Column 2</td>");
                sb.AppendLine("<td>Table 1 - Row 2 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 1 - Row 3 - Column 1</td>");
                sb.AppendLine("<td>Table 1 - Row 3 - Column 2</td>");
                sb.AppendLine("<td>Table 1 - Row 3 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("</table>");
        
                // Вторая таблица (без заголовка)
                sb.AppendLine("<table>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 2 - Row 1 - Column 1</td>");
                sb.AppendLine("<td>Table 2 - Row 1 - Column 2</td>");
                sb.AppendLine("<td>Table 2 - Row 1 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 2 - Row 2 - Column 1</td>");
                sb.AppendLine("<td>Table 2 - Row 2 - Column 2</td>");
                sb.AppendLine("<td>Table 2 - Row 2 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("<tr>");
                sb.AppendLine("<td>Table 2 - Row 3 - Column 1</td>");
                sb.AppendLine("<td>Table 2 - Row 3 - Column 2</td>");
                sb.AppendLine("<td>Table 2 - Row 3 - Column 3</td>");
                sb.AppendLine("</td>");
                sb.AppendLine("</tr>");
                sb.AppendLine("</table>");
        
                sb.AppendLine("</body>");
                sb.AppendLine("</html>");
        
                return sb.ToString();
            }
        
            public DataSet ConvertHTMLTablesToDataSet(String HTML)
            {
                // Локальные сылки
                DataSet ds = new DataSet();
                DataTable dt;
                DataRow dr;
                // DataColumn dc;
        
                bool HeadersExist = false;  // Флаг заголовка таблицы
                int iCurrentColumn = 0;     // Счетчик столбцов
                int iCurrentRow = 0;        // Счетчик строк
        
                // Регулярные выражения
                String TableExpression = "<table[^>]*>(.*?)</table>";
                String HeaderExpression = "<th[^>]*>(.*?)</th>";
                String RowExpression = "<tr[^>]*>(.*?)</tr>";
                String ColumnExpression = "<td[^>]*>(.*?)</td>";
                RegexOptions options = RegexOptions.Multiline |
                                       RegexOptions.Singleline |
                                       RegexOptions.IgnoreCase;
        
                // Заполняем коллекцию соответствий для всех таблиц HTML
                MatchCollection Tables = Regex.Matches(HTML, TableExpression, options);
        
                // Проходим по всем таблицам 
                foreach (Match Table in Tables)
                {
                    // Сбрасываем счетчик строки и флаг заголовка
                    HeadersExist = false;
                    iCurrentRow = 0;
        
                    // Создаем объект новой таблицы и наполняем его
                    dt = new DataTable();
        
                    /* Создаем соответствующее количество столбцов 
                     * для текущей таблицы (используем заголовки, 
                     * если они существуют, иначе используем 
                     * заданные по умолчанию названия)                
                     */
                    if (Table.Value.Contains("<th"))
                    {
                        HeadersExist = true;// Таблица содержит заголовок
                        // Заполняем коллекцию соответствий для всех <th> в таблице
                        MatchCollection Headers = Regex.Matches(
                            Table.Value, HeaderExpression, options);
        
                        // Добавляем столбцы с именами из строки заголовков текущей таблицы
                        foreach (Match Header in Headers)
                        {
                            dt.Columns.Add(Header.Groups[1].ToString());
                        }
                    }
                    else
                    {
                        Match tab = Regex.Matches(Table.Value, TableExpression, options)[0];
                        Match row = Regex.Matches(tab.ToString(), RowExpression, options)[0];
                        int count = Regex.Matches(row.ToString(), ColumnExpression, options).Count;
        
                        // Строки заголовков нет, добавляем дежурные имена
                        for (int iColumns = 0; iColumns < count; iColumns++)
                            dt.Columns.Add("Column" + iColumns);
                    }
        
                    // Извлекаем коллекцию строк текущей таблицы
                    MatchCollection Rows = Regex.Matches(Table.Value, RowExpression, options);
        
                    // Добавляем строки в объект DataRow
                    foreach (Match Row in Rows)
                    {
                        // Игнорируем строку заголовка
                        if (iCurrentRow != 0 || HeadersExist != true)
                        {
                            dr = dt.NewRow();
                            iCurrentColumn = 0;
                            // Извлекаем коллекцию столбцов текущей строки
                            MatchCollection Columns = Regex.Matches(Row.Value, ColumnExpression, options);
                            foreach (Match Column in Columns)
                            {
                                dr[iCurrentColumn] = Column.Groups[1].ToString();
                                iCurrentColumn++;
                            }
        
                            dt.Rows.Add(dr);
                        }
        
                        iCurrentRow++;
                    }
        
                    ds.Tables.Add(dt);
                }
        
                return ds;
            }
        }
    }
  • Удалите в файле Program.cs код выполнения класса Convert1 и добавьте код выполнения класса Convert2. В результате должно получиться следующее
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
        
    namespace ConvertTables
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
        
                Convert2 convert2 = new Convert2();
        
                String source = convert2.GetHTML();
                DataSet dataSet = convert2.ConvertHTMLTablesToDataSet(source);
        
                for (int i = 0; i < dataSet.Tables.Count; i++)
                {
                    // Распечатываем заголовки столбцов
                    foreach (DataColumn col in dataSet.Tables[i].Columns)
                        Console.Write("{0}    ", col.ColumnName);
                    Console.WriteLine();
        
                    // Распечатываем строки
                    foreach (DataRow row in dataSet.Tables[i].Rows)
                    {
                        foreach (DataColumn col in dataSet.Tables[i].Columns)
                            Console.Write("{0}    ", row[col].ToString());
                        Console.WriteLine();
                    }
        
                    Console.WriteLine();
                }
        
                Console.ReadLine(); // Для задержки окна
            }
        }
    }
  • Запустите проект - должен получиться следующий результат
  • Table 1 - Header 1     Table 1 - Header 2     Table 1 - Header 3
    Table 1 - Row 1 - Column 1     Table 1 - Row 1 - Column 2     Table 1 - Row 1 - Column 3
    Table 1 - Row 2 - Column 1     Table 1 - Row 2 - Column 2     Table 1 - Row 2 - Column 3
    Table 1 - Row 3 - Column 1     Table 1 - Row 3 - Column 2     Table 1 - Row 3 - Column 3
    
    Column0     Column1     Column2
    Table 2 - Row 1 - Column 1     Table 2 - Row 1 - Column 2     Table 2 - Row 1 - Column 3
    Table 2 - Row 2 - Column 1     Table 2 - Row 2 - Column 2     Table 2 - Row 2 - Column 3
    Table 2 - Row 3 - Column 1     Table 2 - Row 3 - Column 2     Table 2 - Row 3 - Column 3

    Пример 3. Преобразование группы таблиц с текстами в наборы данных DataSet

    Воспользуемся вторым способом извлечения информации из HTML -таблиц в DataSet и немного изменим задачу. Пусть у нас имеются HTML -файлы исключительно с таблицами текстов одинаковой структуры, например, пусть одна из таблиц будет такой

    QueryID Query CorrectAnswer Comment Theme
    1_4 Каким транспортным средствам в обозначенной знаком зоне запрещена стоянка в выходные и праздничные дни ? 1. Только грузовым автомобилям с разрешенной максимальной массой более 3,5 т. 2. Всем грузовым автомобилям. 3. Всем транспортным средствам. 1 Знак 5.27 "Зона с ограничением стоянки" относится к зональным знакам, требования которых действуют на всей территории (участке дороги), обозначенной такими знаками, вплоть до выезда из зоны, обозначенного в данном случае знаком 5.28 "Конец зоны с ограничением стоянки". Табличка 8.5.1, размещенная под знаком, распространяет его действие только на субботние, воскресные и праздничные дни. А изображение таблички 8.4.1 "Вид транспортного средства" в нижней части знака информирует о том, что в обозначенной зоне стоянка запрещена только грузовым автомобилям, у которых разрешенная максимальная масса превышает 3,5 т. 4
    1_9 По какой траектории Вы имеете право выполнить разворот, управляя автопоездом, имеющим большую длину ? 1. Только по А. 2. Только по Б. 3. По любой. 1 При недостаточной для разворота ширине проезжей части Правила разрешают вне перекрестка выполнение маневра не из крайнего левого положения, а с обочины или от правого края проезжей части (п. 8.8), т.е. по траектории А. 8
    1_10 С какой скоростью Вы можете продолжить движение вне населенного пункта по левой полосе на грузовом автомобиле с разрешенной максимальной массой более 3,5 т ? 1. Не более 50 км/ч. 2. Не менее 50 км/ч и не более 90 км/ч. 3. Не менее 50 км/ч и не более 70 км/ч. 3 Знак 4.6 "Ограничение минимальной скорости" и табличка 8.14 "Полоса движения" предписывают двигаться по левой полосе со скоростью не менее 50 км/ч. Однако при этом на дороге вне населенного пункта, не относящейся к автомагистрали, вы не имеете права развивать скорость на грузовом автомобиле более 70 км/ч (п. 10.3). 10

    Для правильной работы утилиты таблица должна удовлетворять следующим условиям:

  • HTML -файл должен содержать хотя бы одну таблицу
  • Все ячейки таблицы должны содержать параграфы, включая каждый ответ
  • Заголовки таблицы будут являться заголовками столбцов в DataTable
  • Необходимо удалить управляющие символы quot; (кавычки) и nbsp; (пробел) из всей таблицы, иначе они перейдут в DataSet
  • Наша задача, разработать утилиту, которая бы заполнила ADO.NET -объект DataTable или DataSet содержимым одной таблицы <table> или группы таблиц. Утилита будет способна обрабатывать много таблиц, но применим ее только к одной таблице Ticket1.htm, которая приведена в каталоге Source. При желании, можно развить эту утилиту до функциональности, чтобы она создавала и заполняла данными, извлеченными из <table>, таблицу (или несколько таблиц) базы данных.

    В процессе преобразования исходной таблицы HTML будем заполнять наборы данных для вопросов и отдельно для ответов, чтобы получить следующие представления (на примере одной исходной таблицы)

    QueryID Query CorrectAnswer Comment Theme
    4 Каким транспортным средствам в обозначенной знаком зоне запрещена стоянка в выходные и праздничные дни ? 1 Знак 5.27 "Зона с ограничением стоянки" относится к зональным знакам, требования которых действуют на всей территории (участке дороги), обозначенной такими знаками, вплоть до выезда из зоны, обозначенного в данном случае знаком 5.28 "Конец зоны с ограничением стоянки". Табличка 8.5.1, размещенная под знаком, распространяет его действие только на субботние, воскресные и праздничные дни. А изображение таблички 8.4.1 "Вид транспортного средства" в нижней части знака информирует о том, что в обозначенной зоне стоянка запрещена только грузовым автомобилям, у которых разрешенная максимальная масса превышает 3,5 т. 4
    9 По какой траектории Вы имеете право выполнить разворот, управляя автопоездом, имеющим большую длину ? 1 При недостаточной для разворота ширине проезжей части Правила разрешают вне перекрестка выполнение маневра не из крайнего левого положения, а с обочины или от правого края проезжей части (п. 8.8), т.е. по траектории А. 8
    10 С какой скоростью Вы можете продолжить движение вне населенного пункта по левой полосе на грузовом автомобиле с разрешенной максимальной массой более 3,5 т ? 3 Знак 4.6 "Ограничение минимальной скорости" и табличка 8.14 "Полоса движения" предписывают двигаться по левой полосе со скоростью не менее 50 км/ч. Однако при этом на дороге вне населенного пункта, не относящейся к автомагистрали, вы не имеете права развивать скорость на грузовом автомобиле более 70 км/ч (п. 10.3). 10

    QueryID AnswerID Answer
    4 1 Только грузовым автомобилям с разрешенной максимальной массой более 3,5 т.
    4 2 Всем грузовым автомобилям.
    4 3 Всем транспортным средствам.
    9 1 Только по А.
    9 2 Только по Б.
    9 3 По любой.
    10 1 Не более 50 км/ч.
    10 2 Не менее 50 км/ч и не более 90 км/ч.
    10 3 Не менее 50 км/ч и не более 70 км/ч.
  • Добавьте к решению новый проект консольного приложения с именем CreateDataSet и назначьте его стартовым в решении
  • Добавьте к проекту новый класс с именем CreateData в файле CreateData.cs и заполните его так
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
    using System.Text.RegularExpressions;
        
    namespace CreateDataSet
    {
        public enum SelectTicket
        {
            AB,
            CD,
            ABCD
        }
        
        class CreateData
        {
            // Source: "http://www.dotnetfunda.com/articles/article51.aspx"
            void ConvertHTMLTablesToDataSet(
                String HTML, DataSet dsQuestions, DataSet dsAnswers)
            {
                // Локальные сылки
                DataTable dtQuestions;
                DataRow drQuestions;
                DataTable dtAnswers;
                DataRow drAnswers;
        
                bool HeadersExist = false;  // Флаг существования заголовка таблицы
                int iCurrentColumn = 0;     // Счетчик столбцов
                int iCurrentRow = 0;        // Счетчик строк
        
                // Регулярные выражения и опции
                String TableExpression = "<table[^>]*>(.*?)</table>";
                String HeaderExpression = "<th[^>]*>(.*?)</th>";
                String RowExpression = "<tr[^>]*>(.*?)</tr>";
                String ColumnExpression = "<td[^>]*>(.*?)</td>";
                String ParagraphExpression = "<p[^>]*>(.*?)</p>";
                RegexOptions options = RegexOptions.Multiline |
                                       RegexOptions.Singleline |
                                       RegexOptions.IgnoreCase;
        
                // Извлекаем коллекцию таблиц из содержимого HTML
                MatchCollection Tables = Regex.Matches(HTML, TableExpression, options);
        
                // Проходим по всем HTML-таблицам 
                foreach (Match Table in Tables)
                {
                    // Сбрасываем счетчик строки и флаг заголовка
                    HeadersExist = false;
                    iCurrentRow = 0;
        
                    // Создаем объект новой таблицы и наполняем его
                    dtQuestions = new DataTable();
                    dtAnswers = new DataTable();
        
                    /* Создаем соответствующее количество столбцов 
                     * для текущей таблицы (используем заголовки, 
                     * если они существуют, иначе используем 
                     * заданные по умолчанию названия)                
                     */
                    if (Table.Value.Contains("<th"))
                    {
                        HeadersExist = true;// Таблица содержит заголовок
                        // Заполняем коллекцию соответствий для всех <th> в таблице
                        MatchCollection Headers = Regex.Matches(
                            Table.Value, HeaderExpression, options);
        
                        // Добавляем столбцы с именами из строки заголовков текущей таблицы
                        foreach (Match Header in Headers)
                        {
                            // Groups[0] - с дескрипторами <p>Содержимое</p>
                            // Groups[1] - только Содержимое
                            MatchCollection paragraphs = Regex.Matches(Header.Value, ParagraphExpression, options);
                            if (paragraphs.Count != 0)
                                dtQuestions.Columns.Add(paragraphs[0].Groups[1].ToString());
                            else
                                dtQuestions.Columns.Add(Header.Groups[1].ToString());
                        }
                    }
                    else
                    {
                        Match tab = Regex.Matches(Table.Value, TableExpression, options)[0];
                        Match row = Regex.Matches(tab.ToString(), RowExpression, options)[0];
                        int count = Regex.Matches(row.ToString(), ColumnExpression, options).Count;
        
                        // Строки заголовков нет, добавляем дежурные имена
                        for (int iColumns = 0; iColumns < count; iColumns++)
                            dtQuestions.Columns.Add("Column" + iColumns);
                    }
        
                    String[] TableAnswerColumnsName = { "QueryID", "AnswerID", "Answer" };
                    for (int iColumns = 0; iColumns < TableAnswerColumnsName.Length; iColumns++)
                        dtAnswers.Columns.Add(TableAnswerColumnsName[iColumns]);
        
                    // Извлекаем коллекцию строк текущей таблицы
                    MatchCollection Rows = Regex.Matches(Table.Value, RowExpression, options);
        
                    // Добавляем строки в объект DataRow
                    foreach (Match Row in Rows)
                    {
                        // Игнорируем строку заголовка
                        if (iCurrentRow != 0 || HeadersExist != true)
                        {
                            drQuestions = dtQuestions.NewRow();
                            iCurrentColumn = 0;
                            // Извлекаем коллекцию столбцов текущей строки
                            MatchCollection Columns = Regex.Matches(Row.Value, ColumnExpression, options);
                            foreach (Match Column in Columns)
                            {
                                // Извлекаем коллекцию параграфов из текущего столбца
                                MatchCollection paragraphs = Regex.Matches(Column.Value, ParagraphExpression, options);
                                if (paragraphs.Count != 0)
                                {
                                    int iAnswer = 0;
                                    String line = String.Empty;
                                    foreach (Match paragraph in paragraphs)
                                    {
                                        line = paragraph.Groups[1].ToString();
        
                                        // Убираем все лишнее
                                        string[] split = line.Split(new Char[] { '\r', '\n', ' ' });
                                        StringBuilder strBuilder = new StringBuilder();
                                        foreach (string s in split)
                                        {
                                            string word = s.Trim();
                                            if (word != String.Empty)
                                                strBuilder.Append(" " + word);
                                        }
                                        line = strBuilder.ToString().Trim();
        
                                        // Убираем номера ответов
                                        int pos = line.IndexOf(".");
                                        if (pos > 0  pos <= 2  iCurrentColumn == 1)
                                        {
                                            // Ответы 
                                            drAnswers = dtAnswers.NewRow();
                                            drAnswers[0] = drQuestions[0];
                                            iAnswer++;
                                            drAnswers[1] = iAnswer.ToString();
                                            line = line.Substring(pos + 1).Trim();
                                            drAnswers[2] = line;
                                            dtAnswers.Rows.Add(drAnswers);
                                            continue;
                                        }
        
                                        if (iAnswer == 0)
                                            drQuestions[iCurrentColumn] = line;
                                    }
                                }
                                else
                                    drQuestions[iCurrentColumn] = Column.Groups[1].ToString();
        
                                iCurrentColumn++;
                            }
        
                            dtQuestions.Rows.Add(drQuestions);
                        }
        
                        iCurrentRow++;
                    }
        
                    dsQuestions.Tables.Add(dtQuestions);
                    dsAnswers.Tables.Add(dtAnswers);
                }
            }
        
            void FillDataset(string path, DataSet dsQuestions, DataSet dsAnswers, int count)
            {
                for (int i = 1; i <= count; i++)
                {
                    string file = path + "\\Ticket" + i.ToString() + ".htm";
                    System.IO.StreamReader streamReader = new System.IO.StreamReader(
                        file, Encoding.GetEncoding(1251));
        
                    String source = streamReader.ReadToEnd();
        
                    DataSet dsQuestionsItem = new DataSet();
                    DataSet dsAnswersItem = new DataSet();
                    ConvertHTMLTablesToDataSet(source, dsQuestionsItem, dsAnswersItem);
        
                    foreach (DataTable table in dsQuestionsItem.Tables)
                    {
                        DataRow[] rows = new DataRow[table.Rows.Count];
                        for (int iRow = 0; iRow < rows.Length; iRow++)
                        {
                            rows[iRow] = table.Rows[iRow];
                            rows[iRow][0] = (rows[iRow][0]).ToString().Trim();
                        }
                        dsQuestions.Merge(rows);
                    }
                    foreach (DataTable table in dsAnswersItem.Tables)
                    {
                        DataRow[] rows = new DataRow[table.Rows.Count];
                        for (int iRow = 0; iRow < rows.Length; iRow++)
                        {
                            rows[iRow] = table.Rows[iRow];
                            rows[iRow][0] = (rows[iRow][0]).ToString().Trim();
                        }
                        dsAnswers.Merge(rows);
                    }
                }
            }
        
            // Поля
            DataSet dsQuestionsAB = new DataSet();
            DataSet dsAnswersAB = new DataSet();
            DataSet dsQuestionsCD = new DataSet();
            DataSet dsAnswersCD = new DataSet();
            int lenQuery = 0, lenComment = 0, lenAnswer = 0;
            int countTicket = 40; // Количество файлов-билетов
        
            // Свойства
            public DataSet QuestionsAB { get { return dsQuestionsAB; } }
            public DataSet AnswersAB { get { return dsAnswersAB; } }
            public DataSet QuestionsCD { get { return dsQuestionsCD; } }
            public DataSet AnswersCD { get { return dsAnswersCD; } }
            public int LenQuery { get { return lenQuery; } }
            public int LenComment { get { return lenComment; } }
            public int LenAnswer { get { return lenAnswer; } }
            public int CountTicket { set { countTicket = value; } }
        
            public void Execute(SelectTicket selectTicket)
            {
                string path = System.IO.Directory.GetCurrentDirectory();
        
                int len;
                switch (selectTicket)
                {
                    case SelectTicket.AB:
                        // Заполняем DataSet
                        FillDataset(path + @"\AB", dsQuestionsAB, dsAnswersAB, countTicket);
        
                        // Считаем размеры полей для создания объектных таблиц
                        foreach (DataRow row in dsQuestionsAB.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersAB.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        break;
                    case SelectTicket.CD:
                        // Заполняем DataSet
                        FillDataset(path + @"\CD", dsQuestionsCD, dsAnswersCD, countTicket);
        
                        // Считаем размеры полей для создания объектных таблиц
                        foreach (DataRow row in dsQuestionsCD.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersCD.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        break;
                    case SelectTicket.ABCD:
                        // Заполняем DataSet
                        FillDataset(path + @"\AB", dsQuestionsAB, dsAnswersAB, countTicket);
                        FillDataset(path + @"\CD", dsQuestionsCD, dsAnswersCD, countTicket);
        
                        // Считаем размеры полей для создания объектных таблиц
                        foreach (DataRow row in dsQuestionsAB.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersAB.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        foreach (DataRow row in dsQuestionsCD.Tables[0].Rows)
                        {
                            len = row["Query"].ToString().Length;
                            if (len > lenQuery)
                                lenQuery = len;
        
                            len = row["Comment"].ToString().Length;
                            if (len > lenComment)
                                lenComment = len;
                        }
                        foreach (DataRow row in dsAnswersCD.Tables[0].Rows)
                        {
                            len = row["Answer"].ToString().Length;
                            if (len > lenAnswer)
                                lenAnswer = len;
                        }
                        break;
                }
            }
        
            public void Show(DataSet dsQuestions, DataSet dsAnswers)
            {
                for (int i = 0; i < dsQuestions.Tables.Count; i++)
                {
                    // Распечатываем заголовки столбцов
                    foreach (DataColumn col in dsQuestions.Tables[i].Columns)
                        Console.WriteLine("{0}", col.ColumnName);
                    Console.WriteLine();
        
                    // Распечатываем строки
                    foreach (DataRow row in dsQuestions.Tables[i].Rows)
                    {
                        foreach (DataColumn col in dsQuestions.Tables[i].Columns)
                            Console.WriteLine("{0}", row[col].ToString());
                        Console.WriteLine();
                    }
        
                    Console.WriteLine();
                }
        
                for (int i = 0; i < dsAnswers.Tables.Count; i++)
                {
                    // Распечатываем заголовки столбцов
                    foreach (DataColumn col in dsAnswers.Tables[i].Columns)
                        Console.WriteLine("{0}", col.ColumnName);
                    Console.WriteLine();
        
                    // Распечатываем строки
                    foreach (DataRow row in dsAnswers.Tables[i].Rows)
                    {
                        foreach (DataColumn col in dsAnswers.Tables[i].Columns)
                            Console.WriteLine("{0}", row[col].ToString());
                        Console.WriteLine();
                    }
        
                    Console.WriteLine();
                }
            }
        }
    }

    При создании экземпляра этого класса в клиенте должен быть произведен разбор кода HTML -таблиц и заполнены 4 (если режим SelectTicket.ABCD ) набора данных типа DataSet, ссылки на которые доступны через свойства QuestionsAB, AnswersAB, QuestionsCD, AnswersCD. Попутно будут вычислены максимальные размеры полей LenQuery, LenComment, LenAnswer.

  • Модифицируйте файл Program.cs следующим образом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Data;
        
    namespace CreateDataSet
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.WindowWidth = 81;
        
                CreateData tables = new CreateData();
                tables.CountTicket = 1;
                tables.Execute(SelectTicket.CD);
                tables.Show(tables.QuestionsCD, tables.AnswersCD);
        
                Console.WriteLine(String.Format("LenQuery={0}; LenComment={1}; LenAnswer={2}",
                    tables.LenQuery, tables.LenComment, tables.LenAnswer));
        
                Console.ReadLine();
            }
        }
    }
  • Через панель Solution Explorer создайте в корне текущего проекта папку CD и скопируйте в нее контекстной командой Add/Existing Item из прилагаемого к работе каталога Source файл Ticket1.htm
  • В Solution Explorer выделите файл Ticket1.htm и в панели Properties назначьте ему свойство
  • Build Action=Content
  • Copy to Output Directory=Copy if newer
  • Запустите проект на выполнение - получится следующий консольный вывод
  • QueryID
    Query
    CorrectAnswer
    Comment
    Theme 
    1_4
    Каким транспортным средствам в обозначенной знаком зоне запрещена стоянка в выходные и праздничные дни ?
    1
    Знак 5.27 "Зона с ограничением стоянки" относится к зональным знакам, требования которых действуют 
    на всей территории (участке дороги), обозначенной такими знаками, вплоть до выезда из зоны, обозначенного в данном случае 
    знаком 5.28 "Конец зоны с ограничением стоянки". Табличка 8.5.1, размещенная под знаком, распространяет его 
    действие только на субботние, воскресные и праздничные дни. А изображение таблички 8.4.1 "Вид транспортного средства" 
    в нижней части знака информирует о том, что в обозначенной зоне стоянка запрещена только грузовым автомобилям,
    у которых разрешенная максимальная масса превышает 3,5 т.
    4
    1_9
    По какой траектории Вы имеете право выполнить разворот, управляя автопоездом, имеющим большую длину ?
    1
    При недостаточной для разворота ширине проезжей части Правила разрешают вне перекрестка выполнение маневра не из крайнего 
    левого положения, а с обочины или от правого края проезжей части (п. 8.8), т.е. по траектории А.
    8
    1_10
    С какой скоростью Вы можете продолжить движение вне населенного пункта по левой полосе на грузовом автомобиле с 
    С разрешенной максимальной массой более 3,5 т ?
    3
    Знак 4.6 "Ограничение минимальной скорости" и табличка 8.14 "Полоса движения" предписывают двигаться по 
    левой полосе со скоростью не менее 50 км/ч. Однако при этом на дороге вне населенного пункта, не относящейся к автомагистрали, 
    вы не имеете права развивать скорость на грузовом автомобиле более 70 км/ч (п. 10.3).
    10
    QueryID
    AnswerID
    Answer
    1_4
    1
    Только грузовым автомобилям с разрешенной максимальной массой более 3,5 т.
    1_4
    2
    Всем грузовым автомобилям.
    1_4
    3
    Всем транспортным средствам.
    1_9
    1
    Только по А.
    1_9
    2
    Только по Б.
    1_9
    3
    По любой.
    1_10
    1
    Не более 50 км/ч.
    1_10
    2
    Не менее 50 км/ч и не более 90 км/ч.
    1_10
    3
    Не менее 50 км/ч и не более 70 км/ч.
    LenQuery=157; LenComment=615; LenAnswer=74

    Мы видим, что данные извлекаются утилитой из HTML -таблицы правильно и размещаются построчно в соответствующих полях объекта DataSet.

    Упражнение 3. Изменение ширины окна колесом мыши

    В заключение приведем интересный способ регулирования ширины окна WPF, приведенный в http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/a4b5150f-5d70-4d33-9355-4cac9521677d

  • Создайте приложение WPF Application с именем WpfApplication1, внесите в него приведенный ниже код и испытайте работу
  • <Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300"
        Name="window1"  MouseWheel="Window_MouseWheel">
        <Grid>
        </Grid>
    </Window>
    using System;
    using System.Windows;
    using System.Windows.Input;
    using System.Windows.Media.Animation; 
        
    namespace WpfApplication1
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
            {
                int delta = e.Delta;
                double w1 = this.window1.ActualWidth;
                double w2 = w1 + 2 * delta / 10;
                DoubleAnimation anima = new DoubleAnimation();
                anima.Duration = new Duration(TimeSpan.FromSeconds(0.3));
                anima.From = w1;
                anima.To = w2;
                anima.FillBehavior = FillBehavior.HoldEnd;
                this.window1.BeginAnimation(Window.WidthProperty, anima);
            }
        }
    }
  • Внимательно разберитесь со всем приведенным кодом упражнений. Такой подход - самый короткий путь научиться программировать
  • Вернуться к учебному плану