使ってみよう! Windows Live SDK/API

第54回作ってみようSkyDrive連携PowerPointアドイン(後篇)

はじめに

前回に引き続いて、PowerPointアドイン開発の後篇です。スライドショーの最中に最新の写真をSkyDriveからダウンロードしてスライドに表示するアドインを作ります図1⁠。

図1 SkyDriveと連携するPowerPointアドイン
図1 SkyDriveと連携するPowerPointアドイン

Officeアドインで非同期処理

前回でほとんどの準備を行いましたが、メインの処理を書く前に、準備をもうひとつ行います。Officeのアドインでスレッドを使った非同期処理を行う場合、一般的な決まった方法があり、今回はその内容を実装します。

ネットワークアクセスなど時間のかかる処理は、バックグランドで行い、完了時に結果をOfficeアプリ(今回の場合はPowerPoint)に反映したい場合があります。この結果を反映しようとしたとき、Officeアプリがすぐに処理を実行できない可能性があります。その場合、例外が発生するので、アドイン側で再試行などの処理をする方法、または、IMessageFilterインターフェイスを実装する方法があります。今回はIMessageFilterインターフェイスを実装する方法を採ります。詳しくは、Office でのスレッドのサポートなどを参照してください。

IMessageFilterの実装

まず、インターフェイスを用意します。Visual Studioのメニュー[プロジェクト][新しい項目の追加]から、インターフェイスを選択・追加します。IMessageFilter.vbというファイル名にします。そして、次のようにコードを編集してください。

IMessageFilter.vb
Imports System.Runtime.InteropServices
Imports System.Runtime.CompilerServices

<ComImport, ComConversionLoss, InterfaceType(1S), Guid("00000016-0000-0000-C000-000000000046")>
Public Interface IMessageFilter
    <PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType:=MethodCodeType.Runtime)> _
    Function HandleInComingCall(dwCallType As UInteger, htaskCaller As IntPtr, dwTickCount As UInteger, lpInterfaceInfo As IntPtr) As Integer

    <PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType:=MethodCodeType.Runtime)> _
    Function RetryRejectedCall(htaskCallee As IntPtr, dwTickCount As UInteger, dwRejectType As UInteger) As Integer

    <PreserveSig, MethodImpl(MethodImplOptions.InternalCall, MethodCodeType:=MethodCodeType.Runtime)> _
    Function MessagePending(htaskCallee As IntPtr, dwTickCount As UInteger, dwPendingType As UInteger) As Integer
End Interface

このインターフェイスをThisAddInクラスで実装します。次のようにImplementsステートメントでIMessageFilterインターフェイスの実装を指定します。

ThisAddIn.vb
Public Class ThisAddIn
    Implements IMessageFilter

    '(省略)

    Public Function HandleInComingCall(dwCallType As UInteger, htaskCaller As IntPtr, dwTickCount As UInteger, lpInterfaceInfo As IntPtr) As Integer Implements IMessageFilter.HandleInComingCall
    End Function

    Public Function MessagePending(htaskCallee As IntPtr, dwTickCount As UInteger, dwPendingType As UInteger) As Integer Implements IMessageFilter.MessagePending
    End Function

    Public Function RetryRejectedCall(htaskCallee As IntPtr, dwTickCount As UInteger, dwRejectType As UInteger) As Integer Implements IMessageFilter.RetryRejectedCall
    End Function
End Class

IMessageFilterインターフェイスは、3個の関数を持っています。Officeアプリ(PowerPoint)(サーバー側)と、アドイン側(クライアント側)で使う両方の関数が定義されているため、アドインではサーバー側用のHandleInComingCall関数は使いません。ここでは、0を返すように記述しておきます。

Private Const SERVERCALL_ISHANDLED As Integer = 0
Public Function HandleInComingCall(dwCallType As UInteger, htaskCaller As IntPtr, dwTickCount As UInteger, lpInterfaceInfo As IntPtr) As Integer Implements IMessageFilter.HandleInComingCall
    Return SERVERCALL_ISHANDLED
End Function

Officeアプリがすぐに要求した処理を行えない場合、RetryRejectedCall関数が呼ばれます。-1を返すと処理をキャンセルし、0以上100未満の値を返すとすぐにリトライします。100以上の場合は、時間を置いた後にリトライを行います。

通常はダイアログを表示して、再試行するかユーザーに問いますが、今回の作るアドインではスライドショーの最中(プレゼン中)に発生するため、ユーザーに確認せず再試行するよう記述しておきます。ただし、Officeアプリ側が処理を拒否している場合は、処理をキャンセルして終わります。Officeアプリの応答は、引数のdwRejectTypeの値で判断します。

Private Const SERVERCALL_RETRYLATER As Integer = 2
Public Function RetryRejectedCall(htaskCallee As IntPtr, dwTickCount As UInteger, dwRejectType As UInteger) As Integer Implements IMessageFilter.RetryRejectedCall
    If dwRejectType = SERVERCALL_RETRYLATER Then
        Return 100
    Else
        Return -1
    End If
End Function

MessagePending関数は、Officeアプリの応答を待っている間に、何かしらのメッセージをアドインが受けた場合に呼ばれます。

Private Const PENDINGMSG_WAITDEFPROCESS As Integer = 2
Public Function MessagePending(htaskCallee As IntPtr, dwTickCount As UInteger, dwPendingType As UInteger) As Integer Implements IMessageFilter.MessagePending
    Return PENDINGMSG_WAITDEFPROCESS
End Function

以上で、実装部分は終わりです。

MessageFilterの登録

IMessageFilterインターフェイスを実装しただけでは意味がありません。非同期処理を行うときに、毎回 非同期で動いているスレッドからMessageFilterの登録を行います。登録および登録の解除は、CoRegisterMessageFilter関数を使います。

次のようにThisAddInクラスに定義を追加しましょう。

' (ファイル先頭に Imports System.Runtime.InteropServices も追加すること)
<DllImport("ole32.dll")>
Private Shared Function CoRegisterMessageFilter(lpMessageFilter As IMessageFilter, ByRef lplpMessageFilter As IMessageFilter) As Integer
End Function

続いて、前回に記述した非同期で処理を行いOfficeアプリのUIを変更しているGetUserInfoメソッドを修正します。始めと終わりにCoRegisterMessageFilter関数を呼ぶように変更しました。

Private Sub GetUserInfo(session As LiveConnectSession)
    Dim previousMessageFilter As IMessageFilter = Nothing
    CoRegisterMessageFilter(Me, previousMessageFilter) ' 登録

    Me.LiveConnectClient = New LiveConnectClient(session)
    Globals.Ribbons.MainRibbon.SignInButton.Enabled = False

    Try
        ' (ここの内容は前回と同じ)

    Catch ex As Exception
        ' (例外は無視)
        ' 再度サインインできるようボタンを有効化
        Globals.Ribbons.MainRibbon.SignInButton.Enabled = True
    Finally
        CoRegisterMessageFilter(Nothing, previousMessageFilter) ' 解除
    End Try
End Sub

以上で、前回の修正は完了です。

マーカーの追加

それでは、アドインのメインとなる処理を書いていきましょう。まずは、マーカーの追加ボタンをクリックしたときの処理です。前回の内容では、ThisAddInクラスにAddMarkerメソッドを用意するところまで完了しています。

作成するアドインでは、オートシェイプの図形をスライドに貼り付け、スライドショー中にSkyDriveから写真をダウンロードし、オートシェイプの位置に表示するようにします。このマーカーとなるオートシェイプの追加処理を行います。

現在のスライドへ四角形のオートシェイプの追加は次のように記述できます。追加する位置と大きさは決め打ちにしています。

Dim w = Application.ActiveWindow
Dim slide = DirectCast(w.View.Slide, PowerPoint.Slide)
Dim shape = w.Presentation.Slides(slide.SlideIndex).Shapes.AddShape(Microsoft.Office.Core.MsoAutoShapeType.msoShapeRectangle, 0, 0, 100, 100)

アドイン側で、マーカーとして追加したオートシェイプをほかのオートシェイプと区別できるようTagsプロパティを使って区別しておきます。また、今回作るアドインでは、最新の写真1枚のダウンロードにしか対応しませんので、既にマーカーが追加されていた場合は、削除して新たなマーカーを追加することにします。AddMarkerメソッドの全体のコードは次のようになります。

Private Const TagName As String = "SampleAddInObject"

Sub AddMarker()
    Try
        Dim w = Application.ActiveWindow
        Dim slide = DirectCast(w.View.Slide, PowerPoint.Slide)

        ' 既にあるマーカーを削除
        For i = slide.Shapes.Count To 1 Step -1
            Dim s = slide.Shapes(i)
            If s.Tags.Item(TagName) = "Marker" Then
                s.Delete()
            End If
        Next

        Dim shape = w.Presentation.Slides(slide.SlideIndex).Shapes.AddShape(Microsoft.Office.Core.MsoAutoShapeType.msoShapeRectangle, 0, 0, 100, 100)
        shape.Tags.Add(TagName, "Marker")
    Catch ex As Exception
        ' (例外は無視)
    End Try
End Sub

マーカーの追加ボタンがクリックされたとき、必ず編集できるスライドがあるとは限りませんので、その場合は例外が発生します。上記のコードでは、単純にすべての例外を無視するようにしています。

ここまでを実行してみましょう図2⁠。マーカーの追加ボタンをクリックすると、オートシェイプが追加されましたか? このオートシェイプを移動や大きさを変えて写真を表示する位置を指定します。

図2 マーカーの追加
図2 マーカーの追加

スライドショー時の処理

次は、スライドショー中にスライドを切り替えた時の処理を記述します。Application.SlideShowNextSlideイベントで、スライドが表示されたときに処理を行います。

作成するアドインでは、プレゼン中にマーカーのあるスライドの直前まで表示したときに、SkyDriveから写真をダウンロードするようにします。

現在表示されているスライドの次のスライドにマーカーがあるかを確認し、ある場合に、SkyDriveから写真をダウンロードして次のスライドに追加します。コードは次のようになります。

写真のダウンロードと表示は、別途AddPictureメソッドを定義して、その中で処理します。

Private Sub Application_SlideShowNextSlide(Wn As Microsoft.Office.Interop.PowerPoint.SlideShowWindow) Handles Application.SlideShowNextSlide
    If Wn.View.Slide.SlideIndex >= Wn.Presentation.Slides.Count Then
        Exit Sub
    End If

    ' 次のスライド
    Dim slide = Wn.Presentation.Slides(Wn.View.Slide.SlideIndex + 1)

    ' スライドにマーカーがあるかチェック
    Dim shape As PowerPoint.Shape = Nothing
    For Each s As PowerPoint.Shape In slide.Shapes
        If s.Tags.Item(TagName) = "Marker" Then
            shape = s
            Exit For
        End If
    Next
    If shape Is Nothing Then
        Exit Sub
    End If

    ' マーカーがある場合、AddPicture メソッドをスレッドで処理する
    Dim t = New Threading.Thread(AddressOf AddPicture)
    t.SetApartmentState(Threading.ApartmentState.STA)
    t.Start(New Tuple(Of PowerPoint.Slide, PowerPoint.Shape)(slide, shape))
End Sub

ただし、上記コードでは、少し不十分です。既にアドインによってダウンロードした写真がスライド上にある場合は、削除をここで行います。また、サインインしていない状態であれば、ダウンロードできませんので処理を抜けるようにします。サインイン処理は、SignInとGetUserInfoメソッドで記述していました。サインイン時に生成したLiveConnectClientオブジェクトを使ってサインイン状態か判断します。すべて処理を記述したコードは、次のようになります。

Private Sub Application_SlideShowNextSlide(Wn As Microsoft.Office.Interop.PowerPoint.SlideShowWindow) Handles Application.SlideShowNextSlide
    If Wn.View.Slide.SlideIndex >= Wn.Presentation.Slides.Count Then
        Exit Sub
    End If

    ' 次のスライド
    Dim slide = Wn.Presentation.Slides(Wn.View.Slide.SlideIndex + 1)

    ' 以前にアドインで追加された写真を削除
    For i = slide.Shapes.Count To 1 Step -1
        Dim s = slide.Shapes(i)
        If s.Tags.Item(TagName) = "InsertedPicture" Then
            s.Delete()
        End If
    Next

    If Me.LiveConnectClient Is Nothing Then
        Exit Sub
    End If

    ' スライドにマーカーがあるかチェック
    Dim shape As PowerPoint.Shape = Nothing
    For Each s As PowerPoint.Shape In slide.Shapes
        If s.Tags.Item(TagName) = "Marker" Then
            shape = s
            Exit For
        End If
    Next
    If shape Is Nothing Then
        Exit Sub
    End If

    ' マーカーがある場合、AddPicture メソッドをスレッドで処理する
    Dim t = New Threading.Thread(AddressOf AddPicture)
    t.SetApartmentState(Threading.ApartmentState.STA)
    t.Start(New Tuple(Of PowerPoint.Slide, PowerPoint.Shape)(slide, shape))
End Sub

今回のアドインでは、1枚目のスライドにマーカーがある場合や、非表示のスライドがある場合は、考慮していません。

写真のダウンロード

SkyDriveから写真をダウンロードして、スライドに追加する処理を行いましょう。まず、AddPictureメソッドを定義します。このメソッドでは、スライドに追加するOfficeに対する操作があるため、MessageFilterの登録を行います。

Private Sub AddPicture(data As Tuple(Of PowerPoint.Slide, PowerPoint.Shape))
    Dim previousMessageFilter As IMessageFilter = Nothing
    CoRegisterMessageFilter(Me, previousMessageFilter)

    Dim slide = data.Item1
    Dim marker = data.Item2
    Try
        ' (ここに写真のダウンロードとスライドに追加する処理を追記する)
    Catch ex As Exception
        ' (例外は無視)
    Finally
        CoRegisterMessageFilter(Nothing, previousMessageFilter)
    End Try
End Sub

さて、SkyDrive APIのおさらいです。サインインユーザーのSkyDrivet直下のフォルダーとファイルの一覧を取得するには次のURLへHTTP GETアクセスします。

  • https://apis.live.net/v5.0/me/skydrive/files?access_token=ACCESS_TOKEN

また、指定したフォルダーの内容を取得するには次のURLへアクセスします。

  • https://apis.live.net/v5.0/FOLDER_ID/files?access_token=ACCESS_TOKEN

指定したフォルダーから写真を取得してもいいのですが、今回のアドインでは、Windows Phoneで撮影した写真が自動でアップロードされる、カメラロール フォルダーから写真をダウンロードしましょう。このフォルダーは、Folder IDを使わず、特別なフォルダーとして次のようにURLのパスを記述できます。

  • https://apis.live.net/v5.0/me/skydrive/camera_roll/files?access_token=ACCESS_TOKEN

さらに、次のようにURLのパラメーターを指定して、取得枚数と、並び順を指定できます。これによって、最新の1枚を選びます。また、filesではなくphotosとパスを指定すると写真ファイルのみ参照できます。

  • https://apis.live.net/v5.0/me/skydrive/camera_roll/photos?access_token=ACCESS_TOKEN&limit=1&sort_by=updated&sort_order=descending

アドインでは上記のURLにアクセスするようにします。サーバーから受け取るデータは次のようなJSON形式のデータになります。この中から写真のダウンロードのためにsourceと、写真の幅(ピクセル)の取得にwidthheightの値のみ使います。

{
   "data": [
      {
         "id": "file.xxxxx", 
         "from": {
            "name": "梓 中野", 
            "id": "xxxxx"
         }, 
         "name": "WP_001111.jpg", 
         "description": null, 
         "parent_id": "folder.xxxxx", 
         "size": 181761, 
         "comments_count": 0, 
         "comments_enabled": false, 
         "tags_count": 0, 
         "tags_enabled": true, 
         "is_embeddable": true, 
         "picture": "http://storage.live.com/xxxxx/WP_001111.jpg:Thumbnail/WP_001111.jpg", 
         "source": "http://storage.live.com/xxxxx/WP_001111.jpg:Default,Largest/WP_001111.jpg", 
         "upload_location": "https://apis.live.net/v5.0/file.xxxxx/content/", 
         "images": [
         ... 省略 ...
         ], 
         "link": "https://skydrive.live.com/redir.aspx?cid\xxxxx", 
         "when_taken": "2012-05-24T12:00:00+0000", 
         "height": 538, 
         "width": 717, 
         "type": "photo", 
         "location": {
            "latitude": 35.2034, 
            "longitude": 136.2326
         }, 
         "shared_with": {
            "access": "Just me"
         }, 
         "created_time": "2012-05-23T13:00:00+0000", 
         "updated_time": "2012-05-23T13:00:00+0000"
      }
   ], 
   "paging": {
      "next": "/me/skydrive/camera_roll/files?xxxxx"
   }
}

写真のダウンロードまでをコードにします。LiveサービスにアクセスするLiveConnectClientクラスを用意していたので、これを使います。またJSONから値の取得にはJson.NETを利用します。

' SkyDrive から最新の写真1枚の情報を取得
Dim result = Me.LiveConnectClient.Get(
    "me/skydrive/camera_roll/files?limit=1&sort_by=updated&sort_order=descending")

Dim o = JObject.Parse(result)
Dim src = o("data")(0)("source").ToString
Dim w = o("data")(0)("width").Value(Of Integer)()
Dim h = o("data")(0)("height").Value(Of Integer)()

' 写真のダウンロード
Dim client = New Net.WebClient
Dim file = System.IO.Path.GetTempFileName ' 一時的なファイル名を生成
client.DownloadFile(src, file)

特別に難しいところはないと思います。続いてダウンロードした写真をスライドに追加します。

' マーカーの範囲内に収まるように大きさを調節
Dim left, top, width, height As Single
If h * (marker.Width / w) <= marker.Height Then
    width = marker.Width
    height = h * (marker.Width / w)
    left = marker.Left
    top = marker.Top + (marker.Height - height) / 2
Else
    width = w * (marker.Height / h)
    height = marker.Height
    left = marker.Left + (marker.Width - width) / 2
    top = marker.Top
End If

' 写真をスライドに追加
Dim pic = slide.Shapes.AddPicture(file, Microsoft.Office.Core.MsoTriState.msoFalse, Microsoft.Office.Core.MsoTriState.msoCTrue, left, top, width, height)

pic.Tags.Add(TagName, "InsertedPicture")
pic.Left = left
pic.Top = top
pic.Width = width
pic.Height = height
pic.ZOrder(Microsoft.Office.Core.MsoZOrderCmd.msoBringForward) '最前面に表示

マーカーで示した範囲内に収まるように位置を計算し、slide.Shapes.AddPictureメソッドで写真を追加しています。また、アドインによって追加した写真とわかるように、pic.Tags.Addメソッド部分でタグ情報も付けています。

以上で、AddPictureメソッドの処理を記述できました。

動作の確認

ここまでで、アドインに必要な機能は、ほぼすべて実装できています。実行して動作を確認してみましょう。

  1. サインインボタンからサインインします。
  2. 新しいプレゼンテーションに、スライドを1枚追加します。
  3. 2枚目のスライドにマーカーを追加します図3⁠。
図3 2枚目のスライドへマーカーの追加
図3 2枚目のスライドへマーカーの追加

ここまでの動作は、既に確認しています。それではスライドショーを開始してみましょう。1枚目のスライド表示された時点で、2枚目にマーカーがあるため写真のダウンロードが始まります。スライドを進めて写真が表示されれば成功です図4⁠。

図4 追加された写真
図4 追加された写真

もちろん、SkyDriveに写真があることが前提になります。写真がない場合は、写真情報を取得しようとしている部分で例外が発生しています。Windows Phoneを持っている場合は、実際にSkyDriveへの自動アップロード設定を行い、写真を用意してみてください。または、コード中の「me/skydrive/camera_roll/photos」部分を「me/skydrive/photos」や、直接Folder IDを指定して、写真のある場所にアクセスするように変更してください。

図でもわかるように、写真の下にあるマーカーが見えていますので、マーカーは透明にするといいかもしれません。

サインアウト処理

少し残っている処理を書いていきましょう。サインアウトボタンをクリックしたときの処理は、SignOutメソッドで処理するようにしていました。今回利用しているLive ConnectのAPIでは、明示的なサインアウト用の操作は用意されていません。リボンのボタンの表示を変更し、LiveConnectClientオブジェクトの破棄のみ、ここでは行います。

Sub SignOut()
    Me.LiveConnectClient = Nothing
    Globals.Ribbons.MainRibbon.SignInButton.Visible = True
    Globals.Ribbons.MainRibbon.SignInButton.Enabled = True
    Globals.Ribbons.MainRibbon.SignOutMenu.Visible = False
End Sub

アクセストークンの更新

先ほどの動作の確認では、きちんと写真がダウンロードできたかと思いますが、アクセストークンを使ったリソースのアクセス(今回はSkyDriveの写真へアクセス)は、有効期限があります。プレゼン中にアクセストークンの期限が切れるかもしれません。そこで、写真をダウンロードする前にアクセストークンの更新処理を行いましょう。

アクセストークンの更新には、リフレッシュトークンを追加います。この値は、サインイン時に取得するようにしていました。前回に用意したLiveConnectSessionオブジェクトのRefreshTokenプロパティから参照します。

新しいアクセストークンの取得は、次のURLにアクセスします。

  • https://oauth.live.com/token?client_id=CLIENT_ID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN&redirect_uri=https://oauth.live.com/desktop

LiveAuthClientクラスに、次のコードを追記しましょう。GetSessionメソッドで得たLiveConnectSessionオブジェクトを渡すと、新しいLiveConnectSessionを得るメソッドです。

LiveAuthClient.vb
Function RefleshSession(session As LiveConnectSession) As LiveConnectSession
    Dim uri = New Uri(String.Format("https://oauth.live.com/token?client_id={0}&grant_type=refresh_token&refresh_token={1}&redirect_uri={2}",
                      Me.ClientId, session.RefreshToken, Me.RedirectUri))

    Dim client = New WebClient
    Dim json = client.DownloadString(uri)

    Dim o = JObject.Parse(json)

    If o("error") IsNot Nothing Then
        Return Nothing
    End If

    Dim newSession As New LiveConnectSession(
        o("access_token").ToString(),
        o("refresh_token").ToString(),
        New DateTimeOffset(Now.ToUniversalTime).AddSeconds(o("expires_in").ToObject(Of Integer)),
        o("scope").ToString.Split(" "c))

    Return newSession
End Function

AddPictureメソッド内の、LiveConnectClient.Getメソッドを呼ぶ直前にコードを追記します。

' LiveConnectClient の更新
Dim authClient = New LiveAuthClient(ThisAddIn.ClientId)
Dim newSession = authClient.RefleshSession(Me.LiveConnectClient.Session)
Me.LiveConnectClient = New LiveConnectClient(newSession)

ここでは単純に必ずアクセストークンを更新するようにしています。LiveConnectSessionオブジェクトは、期限情報も持っているためもう少し高度に処理することもできます。

おわりに

SkyDriveと連携したPowerPointアドインの開発は以上です。いかがでしたか。これまでの連載内容の一区切りということで、少しだけ実践的なアプリを作ってみました。ダウンロードに失敗したときの対応や、ユーザーが指定したフォルダーにある写真をダウンロードするようになど改善するポイントはいろいろあると思います。ここまで作ったみた方は、ぜひ改良もしてみてください。

おすすめ記事

記事・ニュース一覧