エンコーディングを変更する

エンコーディングを変更する

日本語の読み書きには、不幸なことに、かかせないエンコーディング。Safari は、 public beta の段階で日本語エンコーディング関連で、たくさんの不具合があることで 有名だったよな。1.0 になっても、まだ問題が残っているけど。そんな、エンコーディ ング機能の解説だ。ここでは、エンコーディングを設定するポップアップメニューを付けてみることにしよう。

9.1 デフォルトエンコーディングとカスタムエンコーディング

WebKit のエンコーディングには、アプリケーション全体で使われるデフォルトのエ ンコーディングと、WebView ごとに使われるカスタムのエンコーディングがあるよう だ。デフォルトのエンコーディングは、WebPreferences で設定する。これは後で説 明しよう。カスタムエンコーディングは、WebView の setCustomTextEncodingName: で設定する。

WebKit/WebView.h

- (void)setCustomTextEncodingName:(NSString*)encodingName;

WebView に対してこれを呼んでやれば、好きなエンコーディングを割り当てること ができるぜ。引数のencodingName には、IANA でのエンコーディング名を使うよう だ。これは、Core Foundation を使うと、簡単に取得することができる。


9.2 実装


■エンコーディングポップアップメニューの追加

では、実装を。ここでは、アクセスしやすいように、ポップアップメニューとして実 装してみた。エンコーディングメニューにこれほどアクセスする必要があるとは、日本 語圏の人にしか分かるまいに。前のプロジェクトに追加する形で、実装を進めるぜ。

1. MyDocument.nib を開く。MyDocument.nib をダブルクリックして、InterfaceBuilder を立ち上げてくれ。

2. ウィンドウに、エンコーディングのためのポップアップメニューを追加しよう。下 の図のように、URL テキストフィールドの下に入れてみた。メニューの内容はプログラム内部で変更するので、そのままでいいよ。

popup.jpg
図9-1 ウィンドウのレイアウト

3. MyDocument クラスにアウトレットとアクションを追加する。Classes タブに移動して、MyDocument を選択してくれ。ポップアップのためのアウトレット encodingPopup と、ポップアップのアクションを受けるための changeEncoding: を追加してくれ。

outlets-2.jpg
図9-2 MyDocument のアウトレット

actions-3.jpg
図9-3 MyDocument のアクション

4. アウトレットとアクションを接続する。Instances タブに戻ってくれ。

まずは、アウトレットの接続を。File's Owner を選択して、Ctrl キーを押しながら ドラッグしてくれ。伸びてきた線をウィンドウ上のポップアップに接続して、インスペ クタパネルの encodingPopup と「Connect」する。続いて、アクションの接続。今 度はポップアップを選択して、Ctrl キーを押しながらドラッグ。File's Owner につな げたら、changeEncoding: を選択してくれ。

ここまでできたら、Project Builder に戻ろう。


■ソースコードの編集

5. MyDocument.h を編集する。Interface Builder で追加したポップアップのための インスタンス変数、encodingPopup を追加しよう。

MyFirstBrowser/MyDocument.h

@interface MyDocument : NSDocument
{
 IBOutlet id webView;
 IBOutlet id urlTextField;
 IBOutlet id loadStatusTextField;
 IBOutlet id loadProgressBar;
 IBOutlet id encodingPopup;
 ...

これだけ。


6. MyDocument.m を編集する。ここでやることは、エンコーディングポップアップ の初期化と、アクションを受けてのエンコーディングの変更だ。

まずは、初期化を。windowControllerDidLoadNib: でやることにしよう。ま ず、ポップアップのアイテムをすべて削除する。そして、エンコーディングの名前を追 加してやるんだ。ただし、最初の1 つ目は「デフォルト」にしてやろう。


MyFirstBrowser/MyDocument.m

- (void)windowControllerDidLoadNib:(NSWindowController*)windowController
{
  ...

  // エンコーディングポップアップの設定をします
  [encodingPopup setBordered:NO];
  [encodingPopup removeAllItems];

  // マニュアルで設定できるエンコーディングの一覧です
  static CFStringEncoding _encodings[] = {
    kCFStringEncodingISOLatin1,
    kCFStringEncodingShiftJIS,
    kCFStringEncodingEUC_JP,
    kCFStringEncodingISO_2022_JP,
    kCFStringEncodingUTF8
  };

  // "Default" を追加します
  [encodingPopup addItemWithTitle:@"Default"];
  int i;
  for (i = 0; i < sizeof(_encodings) / sizeof(NSStringEncoding); i++) {
    // エンコードアイテムを追加します
    [encodingPopup addItemWithTitle:
      (NSString*)CFStringGetNameOfEncoding(_encodings[i])];

    // representedObject として、IANA 名を設定します
    NSString* ianaName;
    ianaName =
      (NSString*)CFStringConvertEncodingToIANACharSetName(
          _encodings[i]);
    [[encodingPopup lastItem] setRepresentedObject:ianaName];
  }
}

ここで宣言されている、_encodings 変数に含まれているエンコーディングが追加 されるんだ。日本語関係のエンコードを入れてみた。エンコーディングの名前や、 IANA 名を取得するために、Core Foundation のAPI を使っているんだ。エンコード の名前は CFStringGetNameOfEncoding() で、IANA での名前は CFStringConvertEncodingToIANACharSetName() で取得できる。

続いて、ポップアップからのアクションメソッドを。Interface Builder で宣言した とおり、changeEncoding: という名前だ。

MyFirstBrowser/MyDocument.m

- (void)changeEncoding:(id)sender
{
  // エンコーディングポップアップから、IANA 名を取得します
  NSString* ianaName;
  ianaName = [[encodingPopup selectedItem] representedObject];

  // WebView にテキストエンコーディングを設定します
  [webView setCustomTextEncodingName:ianaName];
}

ここでやることは、まずエンコーディングポップアップから representedObject を取得する。ここに、IANA 名が入っているはずだ。そして、それをWebView に設 定するだけだ。IANA 名が nil のときは、デフォルトのエンコーディングになる。

7. ビルドして実行する。適当なページを開いて、エンコーディングを変更してみてくれ。

browser-5.jpg
図9-4 MyFirstBrowser6 動作図


■ここまでのプロジェクト:
MyFirstBrowser6.zip








図8-3 Go メニュー

3. Controller クラスを作る。Classes タブに移動して、NSObject のサブクラスを作ってくれ。「Controller」という名前にする。

4. Controller にアウトレットとアクションを追加する。アウトレットは Go メニュー のための goMenu と、履歴アイテムを追加する場所を指定するための historySeparatorItem。アクションは、Clear History を受け付けるためのclearHistory: だ。

outlets-1.jpg
図8-4 Controller のアウトレット

actions-2.jpg
図8-5 Controller のアクション

5. Controller をインスタンス化する。Controller クラスを選択して、「Classes」→ 「Instansiate Controller」メニューを選んでくれ。これで、Instances タブに Controller のインスタンスができるよ。

6. Instances タブに戻って、アウトレットとアクションを接続する。まず、 Controller のアウトレットを接続。Controller を選択して、Ctrl キーを押しながらド ラッグすると、線が伸びてくる。この線を、Go メニューと、Go メニューの一番下の セパレータに接続する。それぞれアウトレットを「Connect」しよう。

Go メニューに接続するときは、下にメニューアイテムが表示されている状態で接続し てくれ。この場合だと NSMenu として接続できる。NSMenuItem として接続したい ときは、一回メニューをクリックして、メニューを隠してから接続する。

続いて、アクションの接続。追加したClear History からCtrl を押しながらドラッ グして、Controller につないでくれ。対応するアクションとして clearHistory: を選択する。

あと、Controller をNSApplcation のデリゲートにしておいてくれ。File's Owner を選択して、Ctrl キーを押しながら Controller にドラッグする。そして、delegateとして接続しおくんだ。

7. Controller のためのファイルを作る。Classes タブに戻って、Controller クラスを 選択してくれ。「Classes」→「Create Files for Controller」メニューを選んで、 Controller.h と Controller.m っていうファイルを作る。

ここまでできたら、Project Builder に戻ろう。

■ソースコードの編集

では、ソースコードを書こう。ここでは、Controller クラスの実装と、 MyDocument の簡単な変更が必要なるんだ。

8. Controller.h を変更する。WebHistoryItem の説明で書いたように、履歴からタイ トルを取得するタイミングは、ちょっといやらしい。そこで、いったん追加された履歴 を保存しておいて、後でタイトルを更新するようにしているんだ。そのために、インス タンス変数を追加しておく。配列である、
suspendedHistoryItems だ。

MyFirstBrowser/Controller.h

@interface Controller : NSObject
{
  IBOutlet id goMenu;
  IBOutlet id historySeparatorItem;

  // 更新途中の HistoryItems を保持します
  NSMutableArray*  suspendedHistoryItems;
}

9. Controller.m を編集する。けっこう長いので、ここではポイントを絞って説明しよ う。完全な実装は、ソースコードの方を見てくれ。


◆WebHistory の設定

まず、初期化のメソッドで、WebHistory のための設定をする。ここでは NSApplication のデリゲートメソッドである、applicationDidFinishLaunching: を使おう。まず、WebHistory のインスタンスを作って、共有するために登録する。 次に、WebHistory からのノーティフィケーションを受け取るための登録をするんだ。

MyFirstBrowser/Controller.m

- (void)applicationDidFinishLaunching:(NSNotification*)notification
{
  // WebHistory のインスタンスを作成します
  WebHistory*history;
  history = [[WebHistory alloc] init];

  // WebHistory オブジェクトを共有化します
  [WebHistory setOptionalSharedHistory:history];

  // ノーティフィケーションを登録します
  NSNotificationCenter*  center;
  center = [NSNotificationCenter defaultCenter];

  [center addObserver:self
    selector:@selector(historyAllItemsRemoved:)
    name:WebHistoryAllItemsRemovedNotification
    object:history];
[center addObserver:self
    selector:@selector(historyItemsAdded:)
    name:WebHistoryItemsAddedNotification
    object:history];
}


◆履歴の追加

WebHistory に履歴が追加されると、登録したメソッド historyItemsAdded: が 呼び出される。ここで、追加された WebHistoryItem を取り出して、メニューに追加 するんだ。WebHistoryItem は、ノーティフィケーションの userInfo から取り出すことができる。

MyFirstBrowser/Controller.m

- (void)historyItemsAdded:(NSNotification*)notification
{
  // WebHistroyItem を取得します
  NSArray*     items;
  NSEnumerator*  enumerator;
  WebHistoryItem* item;

  items = [[notification userInfo] objectForKey:WebHistoryItemsKey];
  enumerator = [items objectEnumerator];

  // WebHistroyItem を追加します
  while (item = [enumerator nextObject]) {
    [self _addMenuItemForHistoryItem:item];
  }
}

実際に、メニューに追加するのは、_addMenuItemForHistoryItem: っていうメ ソッドの中だ。ここでメニューを作っているんだけど、実はこのタイミングでは、まだ WebHistoryItem のタイトルを知ることができないんだ。メニューにURL を入れて もいいんだけど、それだと無駄に長くなってしまう。だから、インスタンス変数 suspendedHistoryItems にいったん WebHistoryItem を入れておいて、また別の タイミングで更新するようにしている。

追加した履歴メニューが選択されたときは、メソッド _goToHistoryItem: を呼び 出すように設定しておく。このメソッドの中では、MyDocument の、これから追加す るメソッドを呼び出すようにしているんだ。そこで、選択したURL に飛ぶようにしている。

10. MyDocument.m を変更する。今回は2 つメソッドを追加した。1 つは、指定し たWebHistoryItem のURL に飛ぶための goToHistoryItem: だ。 WebHistoryItem からURL を取り出して、そこに移動する。

MyFirstBrowser/MyDocument.m

- (void)goToHistoryItem:(WebHistoryItem*)historyItem
{
  // URL を文字列型で取り出します
  NSString* urlString;
  urlString = [historyItem URLString];

  // NSURL を作成します
  NSURL* url;
  url = [NSURL URLWithString:urlString];

  // NSURLRequest を作成します
  NSURLRequest* request;
  request = [NSURLRequest requestWithURL:url];

  // メインフレームで、URL を開きます
  [[webView mainFrame] loadRequest:request];
}

もう1つは、WebFrameLoadDelegate のメソッドになる webView:didFinishLoadForFrame:。これは、ページが読み込み終わった時点で呼 ばれる。ここで何をするかというと、履歴メニューの更新をするんだ。この時点なら、 WebHistoryItem のタイトルが取れているはずだ。Controllerの didFnishLoadForFrame: っていうメソッドを呼び出しているんだ。

MyFirstBrowser/MyDocument.m

- (void)webView:(WebView*)sender
      didFinishLoadForFrame:(WebFrame*)frame
{
  // Controller に通知します
  [[NSApp delegate] didFinishLoadForFrame:frame];
}

あとの細かい実装は、ソースコードを見てくれ。

11. ビルドして実行する。履歴メニューの動きを確認してくれ。

browser-4.jpg
図8-6 MyFirstBrowser5 動作図

だけど、なんかアイコンがうまく表示されないんだよな。なんでだろ?

履歴を記録する機能自体は WebKit に含まれているんだけど、いざ使うとなるとけっこう面倒くさい。アプリケーションで必要な履歴機能の仕様を確認しながら、注意して使ってみてくれ。


■ここまでのプロジェクト:
MyFirstBrowser5.zip