【C#】5ちゃんねるビューアーを作ろう(WPF) Final
今回の記事も前回の記事の続きとして書いていきたいと思います。
今回の記事はWPF上でWindowsフォームアプリケーションのコントロールを使う方法を書いていきたいと思います。
ついでに、各テンプレートで定義したビューをちょっと変更してみたいと思います。
heych.hatenablog.com
参照の追加
ソリューションエクスプローラーの「参照」を右クリックして、参照の追加を選択します。
「System.WIndows.Forms」「WindowsFormsIntegration」に☑を入れてOKを押します。
コード
<Window x:Class="_5chViewerWPF.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:_5chViewerWPF" xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms" mc:Ignorable="d" Title="MainWindow" Height="450" Width="600"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="200"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid Grid.Row="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TreeView Grid.Column="0" ItemsSource="{Binding Menus}" SelectedItemChanged="TreeView_SelectedItemChanged"> <TreeView.Resources> <Style TargetType="TreeViewItem"> <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/> </Style> </TreeView.Resources> <TreeView.ItemTemplate> <HierarchicalDataTemplate DataType="{x:Type local:TreeSource}" ItemsSource="{Binding Children}"> <StackPanel> <TextBlock Text="{Binding Text}"/> <TextBlock Text="{Binding URL}"/> </StackPanel> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"/> <ListBox Grid.Column="2" ItemsSource="{Binding Threads}" SelectionChanged="ListBox_SelectionChanged" > <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Text}"/> <TextBlock Text="{Binding URL}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"/> <WindowsFormsHost Grid.Row="2"> <wf:RichTextBox x:Name="richTextBox1" LinkClicked="richTextBox1_LinkClicked" /> </WindowsFormsHost> </Grid> </Window>
- 名前空間宣言のところに「xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"」を追加する
- TemplateにはStackPanelやGridを用いて、複数のコントロールを配置することができる
- 「WindowsFormsHost」は「WindowsFormsIntegration」に含まれているため参照の追加が必要
- 使用するRichTextBoxの名前は「x:Name」で名前を付ける必要がある
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace _5chViewerWPF { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { ViewModel vm = new ViewModel(); public MainWindow() { InitializeComponent(); this.DataContext = vm; string html = ""; //まずHTMLの取得 HttpWebRequest req = WebRequest.CreateHttp("https://www2.5ch.net/5ch.html"); using (var res = req.GetResponse()) { using (var r = res.GetResponseStream()) { using (var sr = new StreamReader(r, Encoding.GetEncoding(932))) { html = sr.ReadToEnd(); } } } //Treeの作成 bool stTag = false; bool edTag = false; bool isAttr = false; string currentTag = null; string currentAttr = null; string tmp = null; Regex regex = new Regex("(?:href=\")(.*)(?:\")"); for (int i = 0; i < html.Length; i++) { if (html[i] == '<') { if (html[i + 1] == '/') { if (tmp != null) { if (currentTag == "B") { TreeSource tn = new TreeSource(tmp); vm.Menus.Add(tn); } else if (currentTag == "a" && vm.Menus.Count > 0) { string href = "https:" + regex.Match(currentAttr).Groups[1].Value + "subback.html"; TreeSource tn = new TreeSource(tmp); tn.URL = href; vm.Menus[vm.Menus.Count - 1].Children.Add(tn); } } edTag = true; } else { stTag = true; tmp = null; currentTag = null; currentAttr = null; } } else if (html[i] == '>') { if (edTag) currentTag = null; stTag = false; edTag = false; isAttr = false; } else if (html[i] == ' ' && stTag) { isAttr = true; } else { if (isAttr) currentAttr += html[i]; else if (stTag) currentTag += html[i]; else if (!edTag && (currentTag == "B" || currentTag == "a")) { tmp += html[i]; if (tmp == "他のサイト") break; } } } } private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { if (((TreeSource)e.NewValue).URL == null) return; menuURL = ((TreeSource)e.NewValue).URL; string html = ""; Regex regex = new Regex("(?:<a href=\")(.*)(?:\">)(.*)(?:</a>)"); //まずHTMLの取得 try { HttpWebRequest req = WebRequest.CreateHttp(((TreeSource)e.NewValue).URL); using (var res = req.GetResponse()) { using (var r = res.GetResponseStream()) { using (var sr = new StreamReader(r, Encoding.GetEncoding(932))) { html = sr.ReadToEnd(); } } } } catch (Exception ex) { System.Windows.MessageBox.Show("HTMLの取得に失敗しました。", "エラー", MessageBoxButton.OK, MessageBoxImage.Error); } //正規表現でhref属性値とスレッドタイトルを取得する vm.Threads.Clear(); try { var st = html.LastIndexOf("<small id=\"trad\">"); var ed = html.IndexOf("</small>", st); var ms = regex.Matches(html); foreach (Match m in ms) { if (m.Index < st || m.Index > ed) continue; vm.Threads.Add(new Thread(m.Groups[2].Value, m.Groups[1].Value)); } } catch (Exception ex) { System.Windows.MessageBox.Show("HTMLの読取に失敗しました。", "エラー", MessageBoxButton.OK, MessageBoxImage.Error); } } string html = ""; string menuURL = ""; private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (((Thread)e.AddedItems[0]).URL == null) return; var url = menuURL; //https://hayabusa9.5ch.net/newsの形にする url = url.Substring(0, url.Length - ("/subback.html").Length); ////https://hayabusa9.5ch.net/test/read.cgi/news/の形にする url = url.Substring(0, url.LastIndexOf("/")) + "/test/read.cgi" + url.Substring(url.LastIndexOf("/")) + "/"; var sub = ((Thread)e.AddedItems[0]).URL; sub = sub.Substring(0, sub.Length - ("150").Length); url = url + sub; //まずHTMLの取得 try { HttpWebRequest req = WebRequest.CreateHttp(url); using (var res = req.GetResponse()) { using (var r = res.GetResponseStream()) { using (var sr = new StreamReader(r, Encoding.GetEncoding(932))) { html = sr.ReadToEnd(); } } } } catch (Exception ex) { System.Windows.MessageBox.Show("HTMLの取得に失敗しました。", "エラー", MessageBoxButton.OK, MessageBoxImage.Error); } richTextBox1.Text = ""; BackgroundWorker bw = new BackgroundWorker(); bw.DoWork += Bw_DoWork; bw.RunWorkerAsync(); } private void Bw_DoWork(object sender, DoWorkEventArgs e) { Regex numberReg = new Regex("(<span class=\"number\">)(.*?)(</span>)"); Regex nameReg = new Regex("(<span class=\"name\">)(.*?)(</span>)"); Regex dateReg = new Regex("(<span class=\"date\">)(.*?)(</span>)"); Regex uidReg = new Regex("(<span class=\"uid\">)(.*?)(</span>)"); Regex escapedReg = new Regex("(<span class=\"escaped\">)(.*?)(</span>)"); int index = 0; string tmp = ""; while (true) { //numberの取得 var m = numberReg.Match(html, index); if (!m.Success) break; var number = m.Groups[2].Value; index = m.Index; //nameの取得 m = nameReg.Match(html, index); if (!m.Success) break; var name = m.Groups[2].Value; index = m.Index; //dateの取得 m = dateReg.Match(html, index); if (!m.Success) break; var date = m.Groups[2].Value; index = m.Index; //uidの取得 m = uidReg.Match(html, index); if (!m.Success) break; var uid = m.Groups[2].Value; index = m.Index; //escapedの取得 m = escapedReg.Match(html, index); if (!m.Success) break; var escaped = m.Groups[2].Value; escaped = escaped.Replace("<br>", "\r\n"); index = m.Index; //nameとescapedの調整 name = Regex.Replace(name, "<[^>]*?>", ""); escaped = Regex.Replace(escaped, "<[^>]*?>", "").Replace(">", ">").Replace("<", "<").Replace("&", "&"); tmp += number + " " + name + " " + date + " " + uid + "\r\n\r\n" + escaped + "\r\n\r\n\r\n"; } richTextBox1.Invoke((MethodInvoker)delegate () { richTextBox1.Text = tmp; }); } private void richTextBox1_LinkClicked(object sender, LinkClickedEventArgs e) { System.Diagnostics.Process.Start(e.LinkText); } } public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnopertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } ObservableCollection<TreeSource> _Menus = new ObservableCollection<TreeSource>(); public ObservableCollection<TreeSource> Menus { get { return _Menus; } set { _Menus = value; OnopertyChanged("Menus"); } } ObservableCollection<Thread> _Threads = new ObservableCollection<Thread>(); public ObservableCollection<Thread> Threads { get { return _Threads; } set { _Threads = value; OnopertyChanged("Threads"); } } string _Contents = ""; public string Contents { get { return _Contents; } set { _Contents = value; OnopertyChanged("Contents"); } } } public class TreeSource { public TreeSource Parent { get; set; } public ObservableCollection<TreeSource> Children { get; set; } public bool IsExpanded { get; set; } public string Text { get; set; } public string URL { get; set; } public TreeSource(string text) { Text = text; Children = new ObservableCollection<TreeSource>(); } } public class Thread { public string Text { get; set; } public string URL { get; set; } public Thread(string text,string url) { Text = text; URL = url; } } }
- MessageBoxはWindows.Formsのものと区別するため「System.Windows.MessageBox」とする必要がある
- WPFのWindowにはInvokeがないため、「richTextBox1.Invoke」とする必要がある(別スレッドにする場合)