読み込んだ画像に落書きをし、PictureHUBに保存する

2011年11月28日(月)
PROJECT KySS

今回はInkPresenterを使って、「らくがき」した画像の保存処理を紹介します。InkPresenterクラスはオブジェクトの表面にインクを描画するクラスです。

まずは、このプログラムで実装する機能の動作を、下記に解説しておきます。

このサンプルは実機(現時点ではauのIS12T)でも動作しますし、エミュレーターでも動作の確認はできます。エミュレーターの場合は、「らくがき」をして保存した画像は[画像読み込み]ボタンで確認できます。エミュレーターで保存させた場合は、エミュレーターを終了すると、保存した落書き画像はなくなります。

基本的にサンプルの動作確認は、実機(IS12T)で行っています。

実機にプログラムをデプロイし、[画像読み込み]ボタンをクリックします。Picturesのアルバムの中の画像が表示されますので、任意の画像を選択します。選択した画像が「らくがき」アプリケーション上に表示されます。任意の色を選択し、画像上に落書きをすることができます。「らくがき」は[らくがきクリア]ボタンで消去することができますが、一度保存した画像の落書きは消去できません。落書きした画像を[保存]ボタンでPicturesHUBに保存することができます。Picturesから保存した画像を見ることができます。この画像を削除する場合は、右隅下にある[・・・]をタップして表示されるメニューから「削除」を選択します(図1)。

図1:画像を読み込み、落書きをし、PictureHUBに保存した。保存した画像はPicturesから閲覧できる(上)。削除する場合は[・・・]をタップして表示されるメニューの「削除」から削除する(下)(クリックで拡大)

サンプル一式は、会員限定特典としてダウンロードできます。記事末尾をご確認ください。

実機(IS12T)で動かした動画はこちらです。

プロジェクトの作成

VS 2010のメニューから[ファイル(F)/新規作成(N)/プロジェクト(P)]を選択します。次に、「Windows Phone アプリケーション」を選択して、「名前(N)」に任意のプロジェクト名を指定します。ここでは「WP71_InkPresenter」という名前を付けています。Windows Phoneのバージョンは7.1を選択します。

ピクチャへのアクセスを提供するMediaLibrayクラスを使用するため、VS2010メニューの「プロジェクト(P)/参照の追加(R)」から、Microsoft.Xna.Frameworkを追加しておいてください。

MainPage.xamlの編集とコントロールの追加

x:NameがPageTitleというTextBlockを削除します。ApplicationTitleというx:NameのTextBlockのTextプロパティに「らくがき」と指定します。

ツールボックスからButtonコントロールを3個、InkPresenterコントロールを1個、InkPresenterコントロールの子要素となるようにImageコントロールを1個、RadioButtonコントロールを4個配置します。

4個のRadioButtonコントロールを全て選択し、プロパティの[共通]パネル内にあるGroupNameプロパティにmyColorと指定します。4つのRadioButtonコントロールのGroupNameが全てmyColorになります。同じ名称を指定していなければ、RadioButtonコントロールに複数チェックが付いてしまいますので、注意してください。Contentプロパティに色名を指定し、Foreground(文字色)にContentプロパティに指定した色名に該当するColorを指定します。[保存]ボタンは最初の状態ではIsEnabledのチェックを外し、選択不可としておきます。全て設定すると図2のような配置になります。

図2:各コントロールを配置した(クリックで拡大)

書き出されるXAMLコードはリスト1のようになります。

リスト1 書き出されたXAMLコード(MainPage.xaml)

(1)<InkPresenter>要素の子要素として<Image>要素を配置しています。
(2)RadioButton1~4までのGroupNameプロパティには全てmyColorと指定しています。myColorは任意の名称で構いません。
<phone:PhoneApplicationPage 
  x:Class="WP71_InkPresenter.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
  xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
  FontFamily="{StaticResource PhoneFontFamilyNormal}"
  FontSize="{StaticResource PhoneFontSizeNormal}"
  Foreground="{StaticResource PhoneForegroundBrush}"
  SupportedOrientations="Portrait" Orientation="Portrait"
  shell:SystemTray.IsVisible="True" Language="ja-JP">

  <!--LayoutRoot は、全てのページ コンテンツが配置されるルート グリッドです-->
  <Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
 
    <!--TitlePanel は、アプリケーション名とページ タイトルを格納します-->
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
      <TextBlock x:Name="ApplicationTitle" Text="らくがき" Style="{StaticResource PhoneTextNormalStyle}"/>
    </StackPanel>
 
    <!--ContentPanel - 追加コンテンツをここに入力します-->
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
      <InkPresenter Height="334" HorizontalAlignment="Left" Margin="28,100,0,0" Name="InkPresenter1" VerticalAlignment="Top" Width="402"> ■(1)
        <Image Canvas.Left="5" Canvas.Top="6" Height="328" Name="Image1" Stretch="Uniform" Width="391" /> ■(1)
      </InkPresenter>
      <Button Content="画像読み込み" Height="73" HorizontalAlignment="Left" Margin="28,21,0,0" Name="readImageButton" VerticalAlignment="Top" Width="402" />
      <RadioButton Content="赤" Height="75" Margin="28,453,321,0" Name="RadioButton1" VerticalAlignment="Top" Foreground="Red" GroupName="myColor" IsChecked="True" /> ■(2)
      <RadioButton Content="青" Foreground="Blue" GroupName="myColor" Height="75" Margin="141,453,208,0" Name="RadioButton2" VerticalAlignment="Top" /> ■(2)
      <RadioButton Content="緑" Foreground="Green" GroupName="myColor" Height="75" Margin="233,453,0,0" Name="RadioButton3" VerticalAlignment="Top" HorizontalAlignment="Left" Width="107" /> ■(2)
      <RadioButton Content="黄" Foreground="Yellow" GroupName="myColor" Height="75" Margin="323,453,26,0" Name="RadioButton4" VerticalAlignment="Top" /> ■(2)
      <Button Content="らくがきクリア" Height="74" HorizontalAlignment="Left" Margin="28,511,0,0" Name="clearButton" VerticalAlignment="Top" Width="402" />
      <Button Content="保存" Height="78" HorizontalAlignment="Left" Margin="28,580,0,0" Name="saveButton" VerticalAlignment="Top" Width="402" IsEnabled="False" />
    </Grid>
  </Grid>
  
  <!--ApplicationBar の使用法を示すサンプル コード-->
 ~コード略~
</phone:PhoneApplicationPage>

次に、MainPage.xamlを展開して表示される、MainPage.xaml.vbをダブルクリックしてリスト2のコードを記述します。

ロジックコードを記述する

リスト2 (MainPage.xaml.vb)

Option Strict On

ランチャーやチューザーに関するクラスの含まれる、Microsoft.Phone.Tasks名前空間をインポートします。
Imports Microsoft.Phone.Tasks

仮想ファイルシステムを作成および使用するための型が含まれている、System.IO.IsolatedStorage名前空間をインポートします。分離ストレージによって、安全なクライアント側のストレージが提供されます。
Imports System.IO.IsolatedStorage

曲、アルバム、再生リスト、およびピクチャを列挙、再生、および表示するためのクラスの含まれる、Microsoft.Xna.Framework.Media名前空間をインポートします。ピクチャへのアクセスを提供するMediaLibrayクラスを使用するため、この名前空間のインポートが必要です。
Imports Microsoft.Xna.Framework.Media

Imports System.Windows.Media.Imaging

インクの操作を行うクラスを提供する、System.Windows.Ink名前空間をインポートします。
Imports System.Windows.Ink

Partial Public Class MainPage
  Inherits PhoneApplicationPage
 
  ' コンストラクター
  Public Sub New()
    InitializeComponent()
  End Sub

単一のインクストローク(インクの一筆)を表す、Strokeクラス用メンバ変数myStrokeを宣言します。
  Dim myStroke As Stroke

[画像読み込み]ボタンがタップされた時の処理

新しいPhotoChooserTaskのインスタンス、photoTaskオブジェクトを生成します。Showメソッドで実行します。 
AddHandlerステートメントで、タスクが完了した時のCompletedイベントハンドラを追加します。Completedイベント内では、完了イベントがTaskResult.OKで、正常に完了した場合は以下の処理を行います。 
chooser での選択の結果は引数で渡されますので、SetSourceメソッドで、渡された画像を BitmapImage オブジェクトに格納します。Image1のSourceプロパティにchooserで選択した画像を格納している、BitmapImageオブジェクトを指定します。[保存]ボタンの使用を可能とします。
完了イベントが正常に完了しなかった場合は、[保存]ボタンは使用不可のままで、処理を抜けます。[画像読み込み]ボタンをタップして、PictureHUBが表示されている場合に、画像を選択しないで、戻るボタン(←)をタップした場合などです。
  Private Sub readImageButton_Click(sender As Object, e As System.Windows.RoutedEventArgs) Handles readImageButton.Click
    Dim photoTask As New PhotoChooserTask
    AddHandler photoTask.Completed, Sub(photoSender As Object, photoArgs As PhotoResult)
                                     If photoArgs.TaskResult = TaskResult.OK Then
                                       Dim bmp As New BitmapImage
                                       bmp.SetSource(photoArgs.ChosenPhoto)
                                       Image1.Source = bmp
                                       saveButton.IsEnabled = True
                                     Else
                                       saveButton.IsEnabled = False  
                                       Exit Sub
                                     End If
                                   End Sub
    photoTask.Show()
  End Sub

[らくがきクリア]ボタンがタップされた時の処理

インクのストロークをクリアします。落書きをクリアします。
  Private Sub clearButton_Click(sender As Object, e As System.Windows.RoutedEventArgs) Handles clearButton.Click
    InkPresenter1.Strokes.Clear()
  End Sub

[保存]ボタンがクリックされた時の処理

保存する画像のファイル名を作成します。現在の「年月日時間分秒.jpg」のファイル名とし、imageFileNameに格納しておきます。
書き込みおよび更新が可能な BitmapSourceを提供する、WriteableBitmapクラス型のmyWriteableBitmap変数を宣言します。InkPresenter1で初期化された、新しいWriteableBitmapのインスタンスmyWriteableBitmapオブジェクトを生成します。WriteableBitmapクラスの第2引数には、ビットマップに特定の変換を適用するTransformを指定できますが、ここではNothingを指定します。
変数storageを、ファイルとディレクトリを格納している分離ストレージ領域を表すIsolateStorageFileクラスとして宣言します。FileExistsメソッドでimageFileNameに格納しているファイルが存在しているかどうかをチェックし、存在している場合は、DeleteFileメソッドで同名ファイルを削除します。
分離ストレージ内のファイルを表すIsolatedStorageFileStreamクラス用オブジェクト変数myStream変数を用意し、IsolatedStorageFile.CreateFileメソッドで、分離ストレージ内にimageFileName変数の持っているファイルを作成します。
Extensions.SaveJpegメソッドで、WriteableBitmapオブジェクトを、JPEGストリームにエンコードします。これは、JPEGファイルのターゲットとなる幅と高さを設定するためのパラメータを持っています。書式は下記の通りです。
Extensions.SaveJpeg(WriteableBitmapオブジェクト,イメージデータストリーム,WriteableBitmapオブジェクトのPixelWidth, WriteableBitmapオブジェクトのPixelHeight,0(固定),0~100の間の写真の品質(70以上を指定))
ピクチャへのアクセスを提供する新しいMediaLibrayクラスのインスタンス、myLibrayを作成します。分離ストレージ内のファイルを表すIsolatedStorageFileStreamクラス用オブジェクト変数Stream変数を用意し、IsolatedStorageFile.OpenFileメソッドで、imageFileNameに格納されているJPEGファイルを、指定したファイルアクセスを使用して指定したモードで開きます。MediaLibrary.SavePictureメソッドで、ストリームオブジェクトに含まれる画像を、メディアライブラリーに保存し、その保存した画像をピクチャオブジェクトとして返します。保存した旨のメッセージを表示します。
  Private Sub saveButton_Click(sender As Object, e As System.Windows.RoutedEventArgs) Handles saveButton.Click
    Dim imageFileName As String=DateTime.Now.ToString("yyyyMMddHHmmss") & ".jpg"
 
    Dim myWriteableBitmap As WriteableBitmap
    myWriteableBitmap = New WriteableBitmap(InkPresenter1, Nothing)
 
    Dim storage As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication
    If storage.FileExists(imageFileName) = True Then
      storage.DeleteFile(imageFileName)
    End If
    Using myStream As IsolatedStorageFileStream = storage.CreateFile(imageFileName)
      System.Windows.Media.Imaging.Extensions.SaveJpeg(myWriteableBitmap, myStream, myWriteableBitmap.PixelWidth, myWriteableBitmap.PixelHeight, 0, 85)
    End Using
 
    Dim myLibray As New MediaLibrary
    Using Stream As IsolatedStorageFileStream = storage.OpenFile(imageFileName, IO.FileMode.Open, IO.FileAccess.Read)
      myLibray.SavePicture(imageFileName, Stream)
      MessageBox.Show("PicturesHubに保存しました。")
    End Using
  End Sub

InkPresenterからマウス(指)が離れた時の処理

インクストロークの関連付けをなくします。
  Private Sub InkPresenter1_MouseLeave(sender As Object, e As System.Windows.Input.MouseEventArgs) Handles InkPresenter1.MouseLeave
    myStroke = Nothing
  End Sub

InkPresenterのマウスの左ボタンが押下された時の処理(InkPresenterが指でホールドされた時の処理)

CaptureMouseメソッドでInkPresenter1にマウスをキャプチャします。
インクストロークを表す新しいインスタンスmyStrokeオブジェクトを作成します。
チェックされたRadioButtonによって、描画されるストロークの色を変えます。
ストロークの幅と高さを7に指定しています。ここの値を変えるとストロークの幅や高さが太くなったり、細くなったりします。
ストロークのスタイラスポイントにAddメソッドで、InkPresenter コントロールに描画されたストロークのスタイラスポイントを追加します。InkPresenter1にスタイラスポイントの追加されたmyStrokeオブジェクトを追加します。
  Private Sub InkPresenter1_MouseLeftButtonDown(sender As Object, e As System.Windows.Input.MouseButtonEventArgs) Handles InkPresenter1.MouseLeftButtonDown
    InkPresenter1.CaptureMouse()
    myStroke = New Stroke()
 
    If RadioButton1.IsChecked = True Then myStroke.DrawingAttributes.Color = Colors.Red
    If RadioButton2.IsChecked = True Then myStroke.DrawingAttributes.Color = Colors.Blue
    If RadioButton3.IsChecked = True Then myStroke.DrawingAttributes.Color = Colors.Green
    If RadioButton4.IsChecked = True Then myStroke.DrawingAttributes.Color = Colors.Yellow
 
    myStroke.DrawingAttributes.Width = 7
    myStroke.DrawingAttributes.Height = 7
    myStroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(InkPresenter1))
    InkPresenter1.Strokes.Add(myStroke)
  End Sub

InkPresenterからマウスの左ボタンが離された時(指が離された)の処理。

インクストロークの関連付けをなくします。ReleaseMouseCaptureメソッドで、InkPresenterがマウスキャプチャを保持していた場合、キャプチャを解放します。
  Private Sub InkPresenter1_MouseLeftButtonUp(sender As Object, e As System.Windows.Input.MouseButtonEventArgs) Handles InkPresenter1.MouseLeftButtonUp
    myStroke = Nothing
    InkPresenter1.ReleaseMouseCapture()
  End Sub

InkPresenter上でマウスを移動させた(指を移動させた)時の処理

インクストロークがアクティブな時は、ストロークのスタイラスポイントにAddメソッドで、InkPresenter コントロールに描画されたストロークのスタイラスポイントを追加します。指でなぞった上をストロークが描画されていきます。
  Private Sub InkPresenter1_MouseMove(sender As Object, e As System.Windows.Input.MouseEventArgs) Handles InkPresenter1.MouseMove
    If myStroke Is Nothing = False Then
      myStroke.StylusPoints.Add(e.StylusDevice.GetStylusPoints(InkPresenter1))
    End If
  End Sub
End Class
  • 「読み込んだ画像に落書きをし、PictureHUBに保存する」サンプルプログラム

四国のSOHO。薬師寺国安(VBプログラマ)と、薬師寺聖(デザイナ、エンジニア)によるコラボレーション・ユニット。1997年6月、Dynamic HTMLとDirectAnimationの普及を目的として結成。共同開発やユニット名義での執筆活動を行う。XMLおよび.NETに関する著書や連載多数。最新刊は「Silverlight実践プログラミング」両名とも、Microsoft MVP for Development Platforms - Client App Dev (Oct 2003-Sep 2012)。http://www.PROJECTKySS.NET/

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています