2014年12月8日月曜日

オフスクリーン描画をライブラリ化(本編2)

(注:数回後の投稿で行なった実験が終わったので、ソースコードの一部を修正しました。修正後の現状が正式版です)

オフスクリーン描画機能をライブラリ化する話の続きです。残りは描画用メソッドだけになったので、それをクラスに追加します。描くのは代表的な要素(図形や文字など)だけですが、それでも十分に役立ちます。

 

メソッドの具体的なコードを書く前に、描画メソッドの処理で考慮すべき点を考えます。

今回作る描画メソッドの多くでは、描画に用いる値をCGContextに設定します。必要な設定を終えてから描画しますが、その後始末はどうしましょうか。数回前の投稿で書いたとおり、CGContextは1つしか持てません。何かの値を変更してしまうと、それが残ったままです。次に別な描画で使う際に、悪影響が出ないとも限りません。

いろいろな描画メソッドを、他からの影響がないか注意しながら作るというのは、メソッド数が増えるほど大変です。もっとも良い解決方法は、他の描画メソッドの影響をまったく受けないように、各メソッドの作り方をルール化することです。

というわけで、描画メソッドの作り方をルール化しました。いろいろな設定はCGContextに対して行なうので、設定前のCGContextを保存しておき、設定変更して描画が終わった時点で、CGContextを元に戻すというルールです。具体的なswiftコードとしては、最初にCGContextSaveGStateを実行してCGContextを保存し、最後にCGContextRestoreGStateを実行してCGContextを元に戻します。これで、描画メソッド内でどんな設定をしても、他の描画メソッドに悪影響を与えません。

CGContextに何かを設定する全メソッドで、このルールを守ります。そうすれば、CGContextをどんなに変更しても気にしなくて済みます。他への影響を考慮しなくてよいというのが、もっとも作りやすい条件ですからね。

 

似たようなメソッドを複数持つ場合には、決めなければならないことが、もう1つあります。引数の並び順です。

並び順は好きに決めて構わないのですが、一般的な法則というか、守ったほうが良いルールはあります。必ず含まれる項目を先に、多く含まれる項目を次に、残りは最後にというルールです。含まれる率が多い項目ほど、前に並べるという考え方です。このルールを守ると、複数あるメソッド引数の並び順が揃って見えるようになります。

ただし、1つだけ注意点があります。関連する項目は近く似合ったほうが使いやすいので、項目をグループ分けしてから、含まれる多さをグループごとで評価するという考え方です。でも、グループ内で含まれる多さが違うと、結果として引数がバラバラに見えるようにもなりかねません。何事もバランスですね。実際に並べてみて判断するしかないようです。

今回のメソッドの引数では、次のような並び順に決めました。「登録色番号、主要属性、座標、大きさ、その他」という順番です。主要属性というのは、図形なら線の太さ、テキストなら文字列や文字サイズ、画像なら画像データなど、それぞれで重要な属性のことです。この並び順に沿って、各メソッドの引数を並べます。ただし、その他の中で何かと関係が深い項目があれば、関係の深い項目の近くに入れるでしょう。

 

図形を描く場合には、決めなければならないことが、もう1つあります。座標の指定方法です。たとえば、長方形を描くときを考えましょう。始点と終点を指定するのか、始点とサイズを指定するのか、どちらかを選ばなければなりません。

すべて統一する考え方もありますが、私の場合は、描くものごとに使い分けたほうが良いと考えました。直線を描く場合は、始点と終点を指定し、長方形や円などの場合は、始点とサイズを指定します。これが一番使いやすいのではと思いました。

 

考慮点が明らかになったので、ようやく直線を描くメソッドを作ります。直線を描くのに必要な情報は、線の太さに加えて、開始点と収容点の座標です。合計で5つの数値を用います。さらに、登録した色の番号も加え、引数は全部で6つとなります。

引数の並び順も最初に決めたルールどおり作ったのが、次のSwiftコードです。

// 直線を描くメソッド
func drawLineF(rNum:Int, _ rLWidth:CGFloat, _ rX1:CGFloat, _ rY1:CGFloat, _ rX2:CGFloat, _ rY2:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DL:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DL:rNumが範囲超え"); return }
    let iContext : CGContext = UIGraphicsGetCurrentContext()
    // 現在のコンテキストを保存
    CGContextSaveGState(iContext)
    // 指定された登録値を、コンテキストに設定
    CGContextSetRGBStrokeColor(iContext, arySColor[rNum][0], arySColor[rNum][1], arySColor[rNum][2], arySColor[rNum][3])
    CGContextSetRGBFillColor(iContext, aryFColor[rNum][0], aryFColor[rNum][1], aryFColor[rNum][2], aryFColor[rNum][3])
    // 引数として受け取った値を、コンテキストに設定
    CGContextSetLineWidth(iContext, rLWidth)
    CGContextMoveToPoint(iContext, rX1, rY1)
    CGContextAddLineToPoint(iContext, rX2, rY2)
    // 線を描画
    CGContextStrokePath(iContext)
    // コンテキストを復帰
    CGContextRestoreGState(iContext)
}

メソッドの最初では、描画可能状態かどうかと、コンテキスト(現状では色のみ)の登録番号が配列の範囲内かを調べています。範囲外なら、エラーメッセージを出して何も描きません。エラーメッセージの形式は、前回と同じです。

配列の範囲内だと確認できたら、現在のコンテキストを保存するのが最初の処理です。メソッドの最後に、この保存したコンテキストを復帰させます。

続いて、CGContextに登録済みの内容(現状では色のみ)を設定します。さらに、引数として受け取った値を、同じくCGContextに設定して、描画する前の準備は完了。あとは、線を描くだけです。最低限の機能しか持たせていないので、メソッドは短くまとまっています。

値チェックの対象として、座標の値も含めることができます。たとえば、最初に設定したスクリーン範囲から、描いたものが外に出ているかをチェックするとかです。しかし、スクリーンの外にはみ出しても、UI部品の設定によっては、意識的に外側へ描かせることも可能です。厳密なチェックを入れると、より自由な使い方を制限しかねません。という理由から、座標の値はチェックしないことに決めました。

 

続いて、長方形を描くメソッドですが、その前に1つ準備が必要です。円や長方形のように、内側に空間のある図形は、枠線に加えて塗りつぶしも描けます。枠線だけなのか、塗りつぶしだけなのか、両方とも描くのか、3種類の中から選ぶ機能が必要です。

図形の形に関係なく、引数として共通で使える型を用意するのが一番です。そのための型を定義しますが、他のクラスでも共通して使えるように、クラスの外に出します。Swiftコードは、次のようにしました。

// 長方形や円を描く際の描画オプション
enum DrawType : Int {
    case None = 0
    case Stroke = 1
    case Fill = 2
    case Both = 3
}

何も描画しない「None」を加えたので、3つではなく、4つになっています。何も描画しない機能ですが、普通は使いません。でも、どう描くのかプログラム内で判断して、描画を使い分ける場合に役立ちます。何も描かない際に、条件分岐でメソッドを呼ばなくする代わりに、「None」を渡せば済み、すっきりしたソースコードで作れます。念のために入れておいた、という位置付けですね。

swiftでは、定義された型については省略形が使えます。「DrawType」型の変数に対して、「.Stroke」や「.Fill」の形で代入や比較が可能です。

 

準備ができたので、ようやく長方形を描くメソッドに取りかかれます。引数としては、定義した色の番号、線の太さ、座標、大きさ、前述のDrawTypeが含まれます。DrawTypeの順番は、最初に決めたルールどおりだと一番最後なのですが、色と関係が深いので、色番号の次に入れることにしました。

こうして出来上がったのが、次のSwiftコードです。全体の構成は、前述の線を描くメソッドと同じになっています。

// 長方形を描くメソッド
func drawRectF(rNum:Int, _ rDType:DrawType, _ rLWidth:CGFloat, _ rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DR:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DR:rNumが範囲超え"); return }
    let iContext : CGContext = UIGraphicsGetCurrentContext()
    // 現在のコンテキストを保存
    CGContextSaveGState(iContext)
    // 指定された登録値を、コンテキストに設定
    CGContextSetRGBStrokeColor(iContext, arySColor[rNum][0], arySColor[rNum][1], arySColor[rNum][2], arySColor[rNum][3])
    CGContextSetRGBFillColor(iContext, aryFColor[rNum][0], aryFColor[rNum][1], aryFColor[rNum][2], aryFColor[rNum][3])
    // 引数として受け取った値を、コンテキストや変数に設定
    CGContextSetLineWidth(iContext, rLWidth)
    let iRect : CGRect = CGRectMake(rX, rY, rWidth, rHeight)
    // 長方形を描画 
    if (rDType == .Fill || rDType == .Both) {
        CGContextFillRect(iContext, iRect)     // 長方形を塗りつぶす
    }
    if (rDType == .Stroke || rDType == .Both) {
        CGContextStrokeRect(iContext, iRect)   // 長方形の枠線を描く
    }
    // コンテキストを復帰
    CGContextRestoreGState(iContext)
}

DrawTypeの値によって、塗りつぶすだけなのか、線だけなのか、両方とも描くのかが決まります。また、両方描いた場合の結果を考慮して、塗りつぶした後に枠線を描くような順序にしてあります。逆順にすると、枠線が細くなってしまいますから。

こちらも最低限の機能しか持たせていないので、コメントを除くと短いコードになっています。

 

円を描くメソッドは、長方形を描くメソッドとほとんど同じです。グラフィックAPIの描く命令を置き換えれば、簡単に作れます。Swiftコードは、次のとおりです。

// 円を描くメソッド
func drawCircleF(rNum:Int, _ rDType:DrawType, _ rLWidth:CGFloat, _ rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DC:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DC:rNumが範囲超え"); return }
    let iContext : CGContext = UIGraphicsGetCurrentContext()
    // 現在のコンテキストを保存
    CGContextSaveGState(iContext)
    // 指定された登録値を、コンテキストに設定
    CGContextSetRGBStrokeColor(iContext, arySColor[rNum][0], arySColor[rNum][1], arySColor[rNum][2], arySColor[rNum][3])
    CGContextSetRGBFillColor(iContext, aryFColor[rNum][0], aryFColor[rNum][1], aryFColor[rNum][2], aryFColor[rNum][3])
    // 引数として受け取った値を、コンテキストや変数に設定
    CGContextSetLineWidth(iContext, rLWidth)
    let iRect : CGRect = CGRectMake(rX, rY, rWidth, rHeight)
    // 円を描画
    if (rDType == .Fill || rDType == .Both) {
        CGContextFillEllipseInRect(iContext, iRect)     // 円内を塗りつぶす
    }
    if (rDType == .Stroke || rDType == .Both) {
        CGContextStrokeEllipseInRect(iContext, iRect)   // 円の枠線を描く
    }
    // コンテキストを復帰
    CGContextRestoreGState(iContext)
}

当然ですが、エラーメッセージの略語も、円を描くメソッド用に変えています。

 

今度は、文字列を描くメソッドです。引数としては、文字列、文字サイズ、座標が含まれます。また、文字の色は、図形用として登録したものをそのまま使えるよう、登録した色番号を指定可能にしました。

文字列を描くためのAPIは、CGContextを使った形ではなく、文字列を扱うNSStringクラスのdrawInRectメソッドを使うようです。以前は別な方法もあったのですが、非推奨になったとか。実際に実験したら上手く動いたので、CGContextと関係ない点が少し納得いかないのですが、使うことにしました。Swiftコードは、次のとおりです。

// 文字列を描く
func drawTextF(rNum:Int, _ rText:NSString, _ rFontSize:CGFloat, _ rX:CGFloat, _ rY:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DT:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DT:rNumが範囲超え"); return }
    // 指定された登録値の色を、UIColorに設定
    let iUIColor : UIColor = UIColor(red:arySColor[rNum][0], green:arySColor[rNum][1], blue:arySColor[rNum][2], alpha: arySColor[rNum][3])
    // 描画の準備:テキスト属性
    let iFont : UIFont = UIFont.systemFontOfSize(rFontSize)
    let iStyle : NSMutableParagraphStyle = NSMutableParagraphStyle()
    iStyle.alignment = .Left
    iStyle.lineBreakMode = .ByWordWrapping
    let iAttr : NSDictionary = [
        NSFontAttributeName: iFont,
        NSParagraphStyleAttributeName: iStyle,
        NSForegroundColorAttributeName: iUIColor,
        NSBackgroundColorAttributeName: UIColor.clearColor()]
    // 描画サイズを求める
    let iCGSize : CGSize = rText.sizeWithAttributes(iAttr)
    let iCGRect : CGRect = CGRectMake(rX, rY, iCGSize.width, iCGSize.height)
    // テキストの描画
    rText.drawInRect(iCGRect, withAttributes:iAttr)
}

見てのとおり、CGContextを使わないので、CGContextの現状を一時保存して復帰させる処理は含まれていません。

文字列の色はUIColorとして用意し、文字属性に入れます。文字の周辺を塗りつぶさないように、バックグラウンドは透明(clearColor)にしました。日本語フォントが選べるほどないため、とりあえずシステムフォント固定にです。将来は、フォント名を指定する形に変更すると思います。また、文字スタイルは使うことがないと判断して、固定にしました。こちらも、もし必要になったときには変更するつもりです。

 

最後に作るのは、UIImageを描くメソッドです。UIImageを受け取って、指定された座標に指定サイズで配置するだけです。クラス名とかぶるようなメソッド名ですが、仕方ないでしょう。登録したコンテキストの値(現状は色のみ)を使っていないので、先頭でのチェックは不要です。Swiftコードは非常に簡単で、次のとおりです。

// UIImageを描くメソッド
func drawImageF(rImage:UIImage, _ rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DI:Disabled"); return }
    let iRect = CGRectMake(rX, rY, rWidth, rHeight)
    rImage.drawInRect(iRect)
}

このメソッドですが、意外にも、非常に重要なのです。このクラスで描いた結果もUIImageで保存します。ですから、このクラスを使って作った部品を、このメソッドを使って、別なイメージに配置したりできます。たとえば、直線を組み合わせ、三角形を描いてUIImageとして作ったとします。このメソッドを使えば、その三角形を、別なイメージに何個も描けます。同じ部品を複数使う場合に役立つでしょう。

また、凝ったデザインで描きたい場合も重宝します。まず、一般的なグラフィック用アプリで複雑な絵(ビットマップ画像)を描き、PNG形式で保存します。それを読み込んでUIImageに設定すれば、このメソッドでそのまま描画できます。ここで用意したメソッドを組み合わせて、絵を作る必要はありません。複雑な形になるほど、プログラムで描くよりも、絵として描いて貼り付けたほうが簡単ですし、仕上がりも良くなります。

PNG形式の絵は、できるだけ大きめに描いて保存し(ただしファイル容量だけは要注意)、必ず縮小して使うようにします。そうすれば、画面表示でも印刷でも、デバイスが持つ最大の解像度が生かせます。

 

以上、長くなってしまいましたが、主な描画メソッドを紹介しました。かなり単純なものしかありませんが、これで意外にも間に合っています。PNG形式の絵を追加すれば、大抵の用途で何とかなりますから。

もっと違う図形、たとえば三角形や五角形を描くメソッドも考えましたが、使用頻度が少なそうなので止めました。これらは、直線を描くメソッドの組み合わせで描け、一旦部品化して何個も配置できますから。もし今後、何か必要になったら、その段階で作るつもりでいます。誰にでも言えることですが、自分専用のライブラリですから、必要な時点で作るという方針で構わないと思います。

 

ここでの説明では、APIの詳しい解説を省きましたので、Appleのサイト資料か、他の親切なブログで調べてください。作る際の考慮点に重点を置いて説明したので、別の面で参考になるのではないかと思います。

いよいよ次回は、ここまで作ったライブラリの上手な使い方を取り上げる予定です。読みやすいソースコードのためには、使い方も意外に重要だったりします。なぜ色を登録する形にしたのか、その理由も明らかにできるでしょう。

 

(使用開発ツール:Xcode 6.0.1, SDK iOS 8.0)

0 件のコメント:

コメントを投稿