ビューの構造
macOS 10.15.7 / Xcode 11.3.1 / Swift 5.0
- 概要
- ビューの構造
- ビューの作成方法
- Y軸の反転(isFlippedプロパティ)
- ビュー上の座標を求める
- ビューの表示位置、サイズを変更する
- 階層化されたビュー(タブビュー)
- ビューに文字列やイメージを表示する(drawメソッド)
- 複合化されたビュー(スクロールビュー)
概要
NSViewクラスおよびその派生クラスは Swiftのウィンドウアプリケーションにあっては、最重要なクラスである。ボタン、テキストフィールド、コンボボックス、テーブルビュー、イメージビュー等のユーザインタフェースに使用する部品は全て、NSViewクラスを継承した NSControllクラスから派生する。
一般的に、ウィンドウアプリケーションは、
ウィンドウ(NSWindow)が、コンテントビュー(NSView)を保持し、その上に各種コントロールオブジェクト(NSControll)を配置するという構造になる。コンテントビューの上にコントロールオブジェクトが配置されると、ビューは2層構造になる。
なお、NSViewクラスは、NSResponderクラスから派生しているが、これは、マウスの操作やキーボード入力等で発生する UIイベントを受け付け処理するクラスである。本章ではこの部分は特に取り上げないが、関連する章があるので必要に応じて参照されたい。
ビューの構造
あらゆるビューオブジェクトは、例えば丸みを帯びたボタンであろうと長方形の外形を持つ。UIコントロールは長方形の中に収まるようにデザイン化される。
ビューオブジェクトの基本属性は、座標とサイズであり frameプロパティに NSRect構造体として保持する。座標(x, y)はビューの起点(長方形の左下角)を示し、親ビューであるコンテントビューの原点からの距離になる。サイズは長方形の幅と高さをピクセル単位で持つ。
ビューの原点は、矩形の左下の点になる。原点の座標(0, 0)から、x軸が右方向、y軸が上方向にに伸びる。長さの単位はピクセルとなる。
例・コンテントビュー上にテキストフィールドを作成する
ビューの作成方法
インタフェースビルダにより作成する
Xcodeのプロジェクトの作成でウィンドアプリケーションを選択すると、通常は、ウィンドウとコンテントビューがデフォルトで作成される。ユーザが行うことは、オブジェクトライブラリからオブジェクトをコンテントビューの上にドロップするだけである。
プログラムにより作成する
コントロール(下記の例ではテキストフィールド)を作成する。イニシャライザの frame引数には、コントロールを配置する始点とサイズを指定した CGRect構造体を指定する。始点は、親ビュー(コンテントビュー)の原点からの相対位置で、
コンテントビューの addSubviewメソッドを実行するとコントロールはここを起点にして表示される。
コントロールを削除(正確には addSubviewメソッドにより貼り付けたビューを親ビューから外す)には、対象のコントロールの removeFromSuperview()メソッドを実行する。インタフェースビルダーで作成したテキストフィールドも削除することができる。
Y軸の反転(isFlippedプロパティ)
ビューの座標系はデカルト座標系であり、原点は左下になるが、これを左上に変更する。NSViewクラスの isFlippedプロパテをオーバーライドすることによりビューの原点を左上に変換することができる。
どういうときに変更するかだが、ひとつには、プログラムから UI画面を作成する場合ではないだろうか。
一般的には、グラフィックのデザイン(例えば画面の設計など)では、座標系の原点は画面の左上になることが多い。Cocoaフレムワークでは、コントロールを作成し、親ビューに配置するには、イニシャライザで貼り付ける位置を指定することになるが、これは原点からの距離であるため、原点が左下だとかなり不自然である。isFlippedプパティの設定により、原点を左上に変更すれば、これらの操作は非常に分かり易いものになるだろう。
isFlippedプロパティは、変更したビューの座標系にだけ効果を持つ。ここでは親ビューに実装すればよい。
原点を左上に変更して前記のテキストフィールドを作成するコードを実行した結果が以下である。
ビュー上の座標を求める
ビュー上の任意の点をクリックしたとき、その座標を求める方法について説明する。ビューの構造は、コンテントビューの上に1個のカスタムビューを貼り付けたものとする。取得できる座標は、基準となる原点の違いにより3種類ある。それぞれについて取得方法を示す。
- コンテントビューを基準としたときのビュー上の座標
- スクリーンを基準としたときのビュー上の座標
- カスタムビュー上をクリックした場合は、カスタムビューを基準としたときのビュー上の座標
コンテントビューを基準としたときのビュー上の座標
コンテントビュービューの起点からの距離となる。mouseDownメソッドを実行すると、NSEventオブジェクトの locationInWindowプロパティにクリックした点の座標(NSPoint)を得ることができるが、これがその値となる。クリックした点がカステムビュー上であっても、コンテントビュービューが基準であることは変わらない。
スクリーンを基準としたときのビュー上の座標
スクリーンの原点もビューと同じように長方形の左下角である。ウィンドウオブジェクトの frameプロパティの origin(ウィンドウの起点)は、スクリーンの原点からの距離に相当する。その点にコンテントビュー上の座標を加算する。クリックした点がカステムビュー上であっても処理は同じである。
NSWindowクラスの convertToScreenメソッドを使用してもウィンドウのスクリーン上における位置を取得することができる。
カスタムビューを基準としたときのビュー上の座標
コンテントビューを基準にした mouseDownメソッドで得られる locationInWindowプロパティの座標をカスタムビューを基準にしたものに変換する。NSViewクラスの convertメソッドは、ビューの座標系を変換するものである。
以下のコードを実行すると、point引数で指定した座標が、from引数で指定したビュー(コンテントビュー)の座標系から、self(カスタムビュー)の座標系に変わる。この点がカスタムビューを基準としたときの座標になる。
[ 補足 ] ウィンド上をマウスでクリックしたときに得られる座標(event.locationInWindow)は「ウィンドウ座標系」である。これは、ビュー座標系とは異なるものであるが、原点は同じく左下である。ただし、ウィンドウ座標系は isFlippedプロパティによる変更を行っても座標系は変わらないので注意すること。
ビューの表示位置、サイズを変更する
ビューの表示位置とサイズは、frameプロパティの値を更新すれば、プログラムから動的に変えることができる。
次の例は、ユーザがウィンドウのサイズを変更したとき、それに合わせて、コンテントビューの中のカスタムビューの表示位置とサイズを変えるものである。サイズはコンテントビューの幅と高さの 1/2を維持し、表示位置はビューの中央にする。
ウィンドウのサイズが変化したとき起動する NSWindowクラスの windowWillResizeメソッドを実装する。この中でウィンドウのサイズに応じて、再表示するカスタムビューのサイズと表示位置を計算し、frameプロパティに代入する。
階層化されたビュー(タブビュー)
ビューの階層は、コンテントビューの上にコントロールオブジェクトを配置する2層構造が一般的だが、ビューを何層にも重ねることは可能である。
実際的な例として、タブをクリックするたびにビューが切り替わるタブビュー(NSTabViewクラス)を取り上げる。次の例は、タブビューオブジェクトをインタフェースビルダで作成し、各ビューにボタンおよびテキストフィールドを、addSubviewメソッドにより貼り付けるたものである。ビューの階層は、コンテントビュー、タブビュー、コントロールの3層になる。
ビューに文字列やイメージを表示する(drawメソッド)
NSViewクラスの drawメソッドを使って(オーバーライドして)、ビュー上に文字列やイメージを描画する。描画には、Cocoaフレームワークの描画システム・グラフィックスコンテキストが利用される。
基本動作・文字列を描画する
引数の dirtyRectにはビューの frameプロパティの値が設定される。この例では枠線で囲まれた カスタムビューの矩形領域 (0.0, 0.0, 220.0, 140.0) となる。
drawメソッドの中で、NSStringクラスの drawメソッドを実行する。text.drawメソッドの in引数には、文字列を描画する矩形領域(NSRect構造体)を指定する。ここでは dirtyRectをそのまま指定している。グラフィックスコンテキストの座標系は左上が原点になるので、ビューの左上を起点にして文字列が描かれる。
![[image10]](image10.png)
描画の開始点を指定する
text.drawメソッドで at引数を指定すると、文字列の描画の開始位置を指定することができる。次の例は、座標(30, 30)の点から文字列を描いているが、この点はビューの座標系に対応しているので、ビューの左下からの距離になる。なお開始点の y値は、正確にいうと文字列の下端になり、この線から上に文字が配置されることに注意。
座標系の変換
isFlippedプロパティの変更により、ビューの左上に変更して上記コードを実行してみた。
イメージの描画
MyViewクラス(イメージを表示するビュー)
NSImageクラスの draw(in: )メソッドを使ってビューにイメージを描画する。in引数には、オーバーライドした draw(_:)メソッドの引数 dirtyRec(CGRrct構造体)を指定するのが普通である。これは、カスタムビューの描画領域であり、本例では、始点(0, 0)、サイズ(400x300)が渡ってくる。イメージはこの値に合わせて時代的に伸縮して表示されるので、ビューがイメージのアスペクト比と異なる場合は画像が歪む。
AppDelegate
![[image13]](image13.png)
複合化されたビュー(スクロールビュー)
例えば、テーブルビューは複数のビューオブジェクトの複合体である。オブジェクトは、NSScrollView、NSClipView、NSTableView、NSTableCellView、NSTextFieldといった NSViewのサブクラスから構成されている。他に重要なクラスに、NSTableColumnクラスがある。テーブルビューの機能は、それぞれの役割を持った複数のクラスが連携することにより実現される。
テーブルビューは単純なビューの階層ではなく、各ビューが機能的に組み合わさったものであり、言わば「複合化されたビュー」と呼べるだろう。このようなオブジェクトの仕組みの一端を示すもとしてスクロールビュー(NSScrollViewクラス)を取り上げてみたい。
次の例は、サイズ 300 x 300 のスクロールビューに、サイズ 400 x 300 のイメージを表示したものである。ビューにはイメージの左上の部分が表示され、スクロールバーの操作で隠れた部分を表示することができる。スクロールビューの高さはイメージの高さと同じだが、スクロールバーの高さの分だけ狭くなるので、若干イメージの下部が隠れる。
![[image14]](image14.png)
実装方法
スクロールビュー(NSScrollクラス)を作成し、コンテントビューに addSubviewメソッドで貼り付ける。
次にカスタムビュー(MyViewクラス)作成し、スクロールビューの documentViewプロパティに代入する。ここがポイントである。なおカスタムビューは、イニシャライザの中で画像ファイルを読み込み、自身のビューに描画している。
注意する点はスクロールビューの contentViewプロパティの frame要素をカスタムビューの frame要素と同じにすることである。contentViewプロパティの設定は、なくても動かないことはまずないが、特定の条件で問題が発生する可能性があり、また異なるバージョンの OSで動作することを保証するため推奨されている。
イメージを描画するビュー(MyViewクラス)
最大のポイントは、イメージを draw(in: ) メソッドで描画するとき、rect引数(CGRect構造体)に dirtyRectではなく、ビューの boundsプロパティを指定することである。
スクロールビューに組み込まれたビューが起動する drawメソッドの dirtyRect引数は、スクロールビューの frame定義に影響を受ける。具体的には、スクロールビューのサイズを超える大きなイメージを描画する場合、表示領域が分割され、それぞれに歪んだイメージを表示してしまう。このため正しいイメージの表示位置とサイズを取得するために boundsプロパティを使用する。
なお、frameプロパティは親ビューの座標系における位置とサイズを表すので、状況によっては表示がずれる等の可能性があるので使用すべきではない。
isFlippedプロパティでビューの座標系を左上原点に設定する。これにより、画像の左上の位置がスクロールビューの左上に来るように配置されるので、自然な見た目となる。