【C#】5ちゃんねるビューアーを作ろう Final of the final
今回の記事は先日書いた記事の続きとして書いていきます。
内容としては「5ちゃんねるビューアー」をWPFのRichTextBoxでやってみようというものです。
heych.hatenablog.com
XAML
まずは、XAMLを以下のような感じにしてRichTextBoxを使用できるようにします。
RichTextBoxは基本的にバインディングできないので、x:Nameを指定してそのDocumentにFlowDocumentを設定してやります。
<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" mc:Ignorable="d" Title="MainWindow" Height="450" Width="600" ResizeMode="CanResizeWithGrip"> <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}"/> </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}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"/> <RichTextBox Grid.Row="2" x:Name="richTextBox1"> <RichTextBox.Resources> <Style TargetType="Hyperlink"> <Setter Property="TextDecorations" Value="Underline"/> <Setter Property="Foreground" Value="Blue"/> <Setter Property="Cursor" Value="Hand"/> </Style> </RichTextBox.Resources> </RichTextBox> </Grid> </Window>
- WIndowに「ResizeMode="CanResizeWithGrip"」をつけることで、ウィンドウのリサイズがやりやすくなる。
コード
コードの方は、
tmp += number + " " + name + " " + date + " " + uid + "\r\n\r\n" + escaped + "\r\n\r\n\r\n";
としていた部分をFlowDocument用に変更してやろうと思います。
また、HyperLinkに関しては正規表現で取得した後HyperLinkに変更してやろうと考えています。
また「>>32」のような返信部分に関してはTooltipを使用してその内容が見れるようにしたいと考えています。
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.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 (vm.Threads.Count<=0 || ((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); } //BackgroundWorker bw = new BackgroundWorker(); //bw.DoWork += Bw_DoWork; //bw.RunWorkerAsync(); 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; FlowDocument doc = new FlowDocument(); List<string> escapeds = new List<string>(); 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("&", "&"); escapeds.Add(escaped);//ツールチップに表示するため用 //tmp += number + " " + name + " " + date + " " + uid + "\r\n\r\n" + escaped + "\r\n\r\n\r\n"; var p1 = new Paragraph(); p1.Inlines.Add(new Run(number)); p1.Inlines.Add(new Run(" " + name)); p1.Inlines.Add(new Run(" " + date)); p1.Inlines.Add(new Run(" " + uid)); p1.Inlines.Add(new LineBreak()); Regex urlreg = new Regex(@"http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"); Regex tagereg = new Regex(@">>?(\d+)"); var p2 = new Paragraph(); string res = ""; for (int i = 0; i < escaped.Length; i++) { string tmp = escaped.Substring(i); var m1 = urlreg.Matches(tmp); var m2 = tagereg.Matches(tmp); if (m1.Count > 0 && m1[0].Index == 0) { p2.Inlines.Add(new Run(res)); res = ""; Hyperlink link = new Hyperlink(new Run(m1[0].Value)); link.NavigateUri = new Uri(m1[0].Value); link.MouseLeftButtonDown += (s, ev) => { System.Diagnostics.Process.Start(((Hyperlink)s).NavigateUri.AbsoluteUri); }; p2.Inlines.Add(link); i += m1[0].Value.Length - 1; } else if (m2.Count > 0 && m2[0].Index == 0) { try { p2.Inlines.Add(new Run(res)); res = ""; var tage = int.Parse(m2[0].Groups[1].Value);//>>99の99 Hyperlink link = new Hyperlink(new Run(m2[0].Value)); var tt = new ToolTip(); var tb = new TextBlock(new Run(escapeds[tage - 1])); tb.TextWrapping = TextWrapping.Wrap; tt.Content = tb; link.ToolTip = tt; link.MouseEnter += (s, ev) => { tt.IsOpen = true; }; link.MouseLeave += (s, ev) => { tt.IsOpen = false; }; p2.Inlines.Add(link); i += m2[0].Value.Length - 1; } catch (Exception ex) { } } else { res += escaped[i]; } } if (res.Length > 0) p2.Inlines.Add(new Run(res)); p2.Inlines.Add(new LineBreak()); doc.Blocks.Add(p1); doc.Blocks.Add(p2); } //別スレッドにするとなぜか以下のようにしてもエラーになってしまう。 //this.Dispatcher.Invoke(() => { richTextBox1.Document = doc; }); richTextBox1.Document = doc; } } public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnoPropertyChanged(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; OnoPropertyChanged("Menus"); } } ObservableCollection<Thread> _Threads = new ObservableCollection<Thread>(); public ObservableCollection<Thread> Threads { get { return _Threads; } set { _Threads = value; OnoPropertyChanged("Threads"); } } string _Contents = ""; public string Contents { get { return _Contents; } set { _Contents = value; OnoPropertyChanged("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; } } }
また、こちらは以下からダウンロードできます。
5chViewerWPF.exe - Google ドライブ