Bakulog

獏の夢日記的な何か。

PowerPointでいらすとやのイラストを使う為のアドイン

久々に.NETでなんかした記事です。

もくじ

  1. 何を作ったのか/使い方
  2. アドインの中はどうなっているのか
  3. WPFで良い感じにする
  4. HtmlAgilityPackで検索しつつ画像をとってくる
  5. PowerPointアドインの流儀に沿って画像を追加する
  6. まとめと補足

1. 何を作ったのか/使い方

PowerPointの拡張として「いらすとやのイラストを検索し、ポチるとスライドに追加できる」というのを作りました。

使い方はGitHubのほうを見てください。

GitHub側にも記載していますが、バイナリ版のインストーラは取りあえずGoogle Driveに突っ込んであります。

 

2. アドインの中はどうなっているのか

GitHubにソースあるので読んでもらえればいいのですが、大きく3セクションに分かれています1

  • WPFのユーザーコントロールで体裁を整える
  • いらすとやのウェブページから指定キーワードでの検索結果を取得し、サムネイルや実際の画像のURLを抽出する
  • 得られた画像をPowerPointアドインの流儀に沿って追加する

それぞれ独立性が高い機能です。

 

3. WPFのユーザーコントロールで体裁を整える

PowerPointアドインはWindows FormのUserControlベースでUIを作らないといけないことになっています。

しかし私はWPFのUserControlが使いたかったため、ElementHostを使って 実質WPFのコントロールでUI作ってるような状況に持ち込みました。

ThisAddIn.cs

using System.Windows.Forms.Integration;
using System.Windows.Forms;
using System.IO;
using Microsoft.Office.Interop.PowerPoint;

//...

private void PrepareCustomControl()
{
    //Windows FormsのUserControl
    var userControl = new UserControl();
    userControl.Controls.Add(new ElementHost()
    {
        //IrasutoyaSearchPane: WPFのほうのUserControl
        Child = new IrasutoyaSearchPane()
        {
            DataContext = new IrasutoyaSearchViewModel()
        },
        //DockStyle.FillにしておくとWPF側のUIがカスタムUI全体に広がる: レイアウト的には無難
        Dock = DockStyle.Fill
    });

    var pane = this.CustomTaskPanes.Add(userControl, "Irasutoya Search");
    pane.Visible = true;
}

一般的なやり方は「WPFコントロールをWindowsフォームに配置する」とかに載っています。

また、今回は検索結果の各アイテムを「サムネイル画像とテキストをセットにしたボタン」 という形で表示したかったので、以下のようにItemsControlDataTemplateを組み合わせて 体裁を整えています。

<UserControl x:Class="Baku.IrasutoyaPpt.IrasutoyaSearchPane"
             ...
             >
    <UserControl.Resources>
        <!-- 検索結果で得られたアイテムを「サムネイラスト + 文字要約」が載ったボタンにする -->
        <DataTemplate x:Key="SearchResultTemplateKey" DataType="{x:Type local:IrasutoyaItemViewModel}">
            <Button Command="{Binding AddToSlideCommand}" Margin="5" HorizontalAlignment="Stretch">
                <StackPanel HorizontalAlignment="Stretch">
                    <Image Margin="3" Source="{Binding ThumbnailUrl, Mode=OneWay}" Stretch="Uniform"/>
                    <TextBlock Margin="3" Text="{Binding ThumbnailText}" TextWrapping="WrapWithOverflow" HorizontalAlignment="Center"/>
                </StackPanel>
            </Button>
        </DataTemplate>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </UserControl.Resources>
    <Grid>
        ...
        <!-- スクロール可能 + 検索結果を一覧表示可能 -->
        <ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled"
                      VerticalScrollBarVisibility="Auto">
            <ItemsControl ItemsSource="{Binding Results}"
                          ItemTemplate="{StaticResource SearchResultTemplateKey}"/>
        </ScrollViewer>
        ...
    </Grid>
</UserControl>

この辺の流れについては「ItemsControl 攻略 ~ 外観のカスタマイズ」とかが参考になります。

もし書籍で似たような事を勉強したい場合は「プログラミングWindows 第6版」あたりを読みましょう。

 

4. HtmlAgilityPackで検索しつつ画像をとってくる

他所のブログにもさんざん書かれている話ですが、いらすとやにはAPIが無いので、 いわゆるウェブスクレイピングが必要です。

C#の場合、HtmlAgilityPackというパッケージがあり、 これを使うと比較的楽ちんに要素抽出ができます。2

やり方は3ステップに区切れます。

  • WebClientなどを使い、検索結果のHTMLを持ってくる
  • HTMLテキストをHtmlAgilityPackに読ませてHtmlDocumentにし、XPathで読める状態に持ち込む
  • XPathを使って良い感じに解析する

アドインのコードよりもサンプルで紹介した方が分かりやすいため、 ここではコンソールアプリケーションの例を示します。

まず新規作成したプロジェクトでNuGetパッケージからHtmlAgilityPackを追加します。 また、参照としてSystem.Webを追加しておきます。 その状態で、次のようなコードを書きます。

using System;
using System.Text;
using System.Linq;
using System.Net;
using System.Web;
using HtmlAgilityPack;

namespace TestIrasutoyaSearch
{
    class Program
    {
        static void Main(string[] args)
        {
            //1. HTMLを拾う
            Console.WriteLine("検索したいワードを入力してください");
            string keyword = Console.ReadLine();

            string url = "http://www.irasutoya.com/search?q=" + HttpUtility.UrlEncode(keyword);

            string html = new WebClient()
            {
                Encoding = Encoding.UTF8
            }.DownloadString(url);

            //2. HtmlDocumentに変換
            var doc = new HtmlDocument();
            doc.LoadHtml(html);

            //3. XPathを渡してサムネイルの要素を指定

            //NOTE: ノードが見つからないとnullで帰ってくる
            var thumbnails = doc
                .DocumentNode
                .SelectNodes(@"//div[@class=""date-outer""]//div[@class=""boxim""]/a")
                ?? Enumerable.Empty<HtmlNode>();

            foreach(HtmlNode node in thumbnails)
            {
                Console.WriteLine($"Thumbnail link: {node.GetAttributeValue("href", "")}");
                Console.WriteLine($"Thumbnail elem: {node.InnerHtml}\n\n");
            }

            //4. 「前のページ」「次のページ」といった要素があればその移動先URLを取得
            string nextPageUrl = doc
                .DocumentNode
                .SelectSingleNode(@"//a[@class=""blog-pager-older-link""]")
                ?.GetAttributeValue("href", "")
                ?? "";
            Console.WriteLine($"Next: {nextPageUrl}");

            //NOTE: 今回の例ではつねに先頭の検索結果を出すため、「前のページ」は無い
            string prevPageUrl = doc
                .DocumentNode
                .SelectSingleNode(@"//a[@class=""blog-pager-newer-link""]")
                ?.GetAttributeValue("href", "")
                ?? "";
            Console.WriteLine($"Prev: {prevPageUrl}");

        }
    }
}

 

実行して「ロボット」を検索した場合の出力を抜粋するとこんな感じになります。

検索したいワードを入力してください
ロボット
Thumbnail link: http://www.irasutoya.com/2016/12/blog-post_911.html
Thumbnail elem: <script type='text/javascript'>
document.write(bp_thumbnail_resize("https://3.bp.blogspot.com/-p8VAdsMe--w/WGHFF1wOnYI/AAAAAAABArg/8dYEgeMpQiIswCGM9dW2-edhW3dRuzOEgCLcB/s72-c/thumbnail_higogata_robot.jpg","いろいろな人型ロボットのイラスト"));
</script>


Thumbnail link: http://www.irasutoya.com/2015/01/blog-post_52.html
Thumbnail elem: <script type='text/javascript'>
document.write(bp_thumbnail_resize("http://1.bp.blogspot.com/-7lDnPmW63NM/VJ6XHeyIqVI/AAAAAAAAqFg/CzKEJobd9BQ/s72-c/omochaya.png","おもちゃ屋のイラスト"));
</script>


Thumbnail link: http://www.irasutoya.com/2013/09/blog-post_589.html
Thumbnail elem: <script type='text/javascript'>
document.write(bp_thumbnail_resize("http://1.bp.blogspot.com/-uPm7jG2nRMs/UbVu_9m0d9I/AAAAAAAAUlc/PpDn9P3KBmc/s72-c/mokei_puramo.png","プラモデルを作っている男の子のイラスト"));
</script>


Next:
Prev:
続行するには何かキーを押してください . . .

  各サムネイル要素に対し、そのサムネイルに対応するイラストページのURLが取得できていることが分かります。 実際のアドインでは、各イラストページに対してもふたたび上と似たような処理を適用し、 最終的にフルサイズ版イラストのURLを取得しています。

上の例では表示されてませんが、ヒット件数が多いキーワードを指定すると、 「Next:」のURLも表示されるようになります。これを使えばページ遷移みたいなことも可能になります。

もし手元で試す場合は、いらすとやの検索ボックスを使った場合と 見比べてみると分かりやすいかと思います。

なお、ここで「この検索に使うXPathの文字列ってどうやったら分かるの?」という疑問が出てくると思います。 基本文法はMSDNで読んだりググると出てきます。

実際にやるときの方法は私の場合こんな感じです。

  1. ウェブブラウザの開発者モードでソースとビジュアル階層を見比べる
  2. 抽出したい要素の階層を見つける
  3. その要素のタグや属性値を見て、うまく特定できそうなXPathをカンで決める

典型的なパターンではclass属性を指定したdivとかで割とうまくいきますが、臨機応変にやってください。

さらにもう一つ。上記コードの処理ではサムネイルの画像URLが完全には拾えません。 JavaScriptが絡んでるのがその原因です。

アドインのコードでは、抽出した要素に含まれるJavaScriptコードを さらに正規表現でバラして、サムネイル画像のURLだけ取り出す、といったこともやっています。 HTMLの中身のテキスト解析はHtmlAgilityPack単体ではどうにもならないので、 うまく諦めて他の方法に切り替えるのも大事なポイントです。

 

5. PowerPointアドインの流儀に沿って画像を追加する

ふたたびアドインのコードに戻って、仕上げの処理です。 HTMLからの処理としては画像URLを抽出し、画像をダウンロードするところまでが可能です。

それをPowerPointの画像に追加する際は、(ちょっとダサいですが、) いったんローカルファイルに保存することで処理が通ります。

ThisAddIn.cs

using System.Windows.Forms.Integration;
using System.Windows.Forms;
using System.IO;
using Microsoft.Office.Interop.PowerPoint;

//... 

public void AddImageToCurrentSlide(byte[] binImage)
{
    string fileName = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
    File.WriteAllBytes(fileName, binImage);

    (this.Application?.ActiveWindow?.View?.Slide as Slide)
        ?.Shapes
        ?.AddPicture(
        fileName, 
        Microsoft.Office.Core.MsoTriState.msoFalse, 
        Microsoft.Office.Core.MsoTriState.msoTrue,
        100, 100
        );
}

現在のスライドを指定する方法((this.Application?.ActiveWindow?.View?.Slide as Slide))も、 知ってると便利そうな小技かもしれません。

 

6. まとめと補足

なんだかんだでOfficeのアドインは作りやすいなあと思いました。

そして最後の最後で今更な補足になりますが、本アドインのネタは私のオリジナルではないです。 ネタ元はTwitterピリ辛(@lifeslash)さんがやっていたもので、 より具体的に言うならこの記事の後発的なマネです。

そちらのケースでは見たところ「WinForms+スクレイピング正規表現縛り」でやってそうに見えたので、 「WPF + HtmlAgilityPackのほうがそれっぽいし手間減るのでは」みたいな直感に基づいて ガーッと作ったのが今回のやつです。

…まあ、この辺は知識背景にもよるので何とも言えませんが。


  1. 社会人のテクニックとして「なんでもいいから3つに分けておけばそれっぽくなる」というライフハックを学びました。 
  2. もちろん一択ではありません。他の例としてはC#でスクレイピング:HTMLパース(Linq to Html)のためのSGMLReader利用法等があります。