広告 |
---|
■□■□■□■□■□■□■□■□■□■□■□■□■□■□■ 2012年02月24日 Java総合講座 - 初心者から達人へのパスポート 2009年11月開講コース 059号 セルゲイ・ランダウ バックナンバー: http://www.flsi.co.jp/Java_text/ ■□■□■□■□■□■□■□■□■□■□■□■□■□■□■ ------------------------------------------------------- ・現在、このメールマガジンは以下の2部構成になっています。 [1] 当初からのコース:vol.xxx(xxxは番号)が振られています。 これは現在、中級レベルになっています。 [2] 2009年11月開講コース:xxx号(xxxは番号)が振られています。 これは現在、初心者向けのレベルになっています。 ・このメールマガジンは、画面を最大化して見てください。 小さな画面で見ていると、不適切な位置で行が切れてしまう など、問題を起すことがあります。 ・このメールマガジンに掲載されているソース・コード及び 文章は特に断らない限り、すべて筆者が著作権を所有してい ます。また、これらのソース・コードは学習用のためだけに 提供しているものです。 ------------------------------------------------------- ======================================================== ◆ 01.グラフィックスのプログラミング ======================================================== 今回は、描いた絵をファイルに保存する機能と保存したファイルを 開く機能を追加しましょう。 その前に画像のファイルの形式について簡単に説明しておきます。 画像のデータやファイルには、大きく分けて2種類の形式(format) があります。ひとつはラスター・グラフィックス(raster graphics)、 と呼ばれ、もうひとつはベクター・グラフィックス(vector graphics) と呼ばれるものです。 ラスター・グラフィックスは、画像を色のついた点の集まりとして記憶 する形式です。写真などは通常、この形式のファイルとして保管されま す。 (ちなみに、新聞や雑誌に掲載されている写真も虫眼鏡で拡大してみる と点の集まりにすぎないことがわかりますね。テレビやディスプレイの 画像も虫眼鏡で拡大してみると点の集まりにすぎないことがわかります。) 一方、ベクター・グラフィックスは、これまでに図を描いてきたときの ように図形の位置や大きさを示す数字や図形の種類、色の種類といった データを記憶する形式です。 典型的な例としてはCAD(Computer Aided Design)で作成する製図の 図面などがこの形式のファイルとして保管されますが、その他のお絵描 きソフトでもこの形式でファイルを保管するものがあります。 このうち、今回はラスター・グラフィックスの形式を使用します。 なお、DrawingPanelで図を描くときに使用する各図形のオブジェクト (DecoratedShapeの各サブクラス)はベクター・グラフィックスの形式で データを(メモリー上に)記憶していますから、これを利用してベクター 形式のままファイルに保管することも容易にできます。 さて、ラスター・グラフィックスのファイル形式には、Windows環境で よく使用されるものとしてビットマップ(bitmap)があります。 このファイルは基本的には圧縮されていません(最近では圧縮されてい るものもあるが圧縮率は低い)。 ビットマップのファイルは、拡張子に.BMPまたは.bmpがつきます。 その他、最近よく使われるようになってきたファイル形式には、JPEG (Joint Photographic Experts Group)とPNG(Portable Network Graphics)があります。 JPEGはラスター・グラフィックスのデータを大幅に圧縮することができ る形式で、できるだけ小さいサイズのデータが好まれるインターネット の世界ではよく使われています。 圧縮率を高くするとその分、画質が劣化するため、一般のグラフィックス のファイル保存には好ましくありませんが、写真画像では劣化が目立た ないためよく使われます。(写真のための形式と考えると好都合です。) JPEGのファイルは、拡張子に.JPEGまたは.jpegまたは.JPGまたは.jpgまた は.JPEまたは.jpeがつきます。 PNGは元々インターネットの世界でよく使われていたGIF(Graphic Interchange Format)という形式に対抗して(GIFにはライセンス料金と いううるさい問題がつきまとっているので)作られた形式で、GIFと同じ ように透明色を指定して背景イメージとの重ねあわせができるなどの機能 を持つ上に、GIFよりも圧縮率が高くなっています。 PNGのファイルは、拡張子に.PNGまたは.pngがつきます。 他にも、さまざまな形式がありますが、省略します。 これらの形式のうち、今回はJPEGのファイルにして保存することにします。 描いた絵をファイルに保存するには、ImageIOというクラスのwrite()メソ ッドを使用します。 (ImageIOクラスはjavax.imageioパッケージにはいっています。) 描いた絵をJPEGのファイルにして保存するには、RenderedImageというイン ターフェースを実装したオブジェクトに描画しておいて、それを -------------------------------------------------------- ImageIO.write(RenderedImage im, "jpg", File outputFile) -------------------------------------------------------- という形式のメソッド呼び出しによって、ファイルに書き込むことができ ます。 (RenderedImageはjava.awt.imageパッケージにはいっています。) ここで、Fileはファイルを表現するクラスで、java.ioパッケージにはいっ ています。 (write()メソッドには他の型の引数を持つものもあります。) このwrite()メソッドの第二引数には、作成したいファイルの形式に応じて 以下のような文字列が指定できます。 JPEG --> 第二引数は"JPEG"または"jpeg"または"JPG"または"jpg"を指定する。 PNG --> 第二引数は"PNG"または"png"を指定する。 ビットマップ --> 第二引数は"BMP"または"bmp"を指定する。 ただし、使用できる形式はプログラムの実行環境によっても異なりますので、 正確にはImageIO.getWriterFormatNames()を実行して調べる必要があります。 RenderedImageを実装したクラスとしてはBufferedImageというクラスがあり ます。これはImageのサブクラスです。したがってImageのメソッドがそのまま 使えます。よってImageのgetGraphics()メソッドも使えます。 (BufferedImageはjava.awt.imageパッケージにはいっています。) たとえば -------------------------------------------------------- BufferedImage rendImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = rendImage.getGraphics(); -------------------------------------------------------- というふうにして生成したBufferedImageオブジェクトからGraphicsオブジェ クトを取り出しておき、その後Graphicsオブジェクトに対して描画を行って おきます。 そうすると、その後 -------------------------------------------------------- File file = new File("newimage.jpg"); // (newimage.jpgはファイル名) ImageIO.write(rendImage, "jpg", file); -------------------------------------------------------- のようにして、描いた絵をファイルに書き込むことができます。 では、さっそくプログラミング作業に入っていきましょう。 Eclipseを起動し、DrawingPanelのエディターを開いてください。 DrawingPanelのpaint()メソッドのソース・コードを見てください。 先頭の -------------------------------------------------------- Image image = createImage(getSize().width, getSize().height); -------------------------------------------------------- によってImageオブジェクトを取り出した後、さらにそこからGraphicsオブジェ クトを取り出して描画を行っていますね。 BufferedImageはImageのサブクラスなので、Imageの振る舞いも継承しています。 そこで、上記のImageオブジェクトに対して行っていた処理部分を抜き出して BufferedImageオブジェクトに対して行わせるようにもできます。 はい、やってみましょう。 まずは、Imageオブジェクトに対して行っていた部分を抜き出して別のメソッ ドにしましょう。 paint()メソッドの -------------------------------------------------------- Graphics2D g2dImage = (Graphics2D) image.getGraphics(); -------------------------------------------------------- の行から最後のほうの -------------------------------------------------------- for (int i = 0; i < shapes.size(); i++) { shapes.get(i).paintSelf(g2dImage); } -------------------------------------------------------- の行までをいっきょに選択状態にして、メニュー・バーの「リファクタリング」 →「メソッドの抽出」を選択し、「メソッドの抽出」ウインドウの「メソッド名」 欄に paintImage と入力し、「アクセス修飾子」はpublicを選択して、「OK」ボタンをクリックし ましょう。(paintImageの代わりに他の名前を入力してもよいが、わかりやすい 名前にすべし。) paintImage()メソッドが自動的に作られ、選択していた複数の行がこちらに移り ましたね。 念のためにpaintImage()メソッドの全容を下に提示しておきます。 -------------------------------------------------------- public void paintImage(Image image) { Graphics2D g2dImage = (Graphics2D) image.getGraphics(); g2dImage.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); DecoratedShape aShape = null; if(shapeType == ShapeEnum.FREECURVE) { aShape = new DecoratedFreeCurve(); if (ptIndex > 9) { ((GeneralPath)aShape.getShape()).moveTo(pts[0].x, pts[0].y); for(int i = 3; i < ptIndex - 6; i += 9) { ((DecoratedFreeCurve)aShape).addCurve(pts[i].x, pts[i].y, pts[i+3].x, pts[i+3].y, pts[i+6].x, pts[i+6].y); } } aShape.setColor(lineColor); aShape.setStroke(drawStroke); } else if(endPoint != null) { if (shapeType == ShapeEnum.LINE) { aShape = new DecoratedLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y); } else { Point tempStartPoint; Point tempEndPoint; int drawWidth; int drawHeight; tempEndPoint = new Point(Math.max(endPoint.x, startPoint.x), Math.max(endPoint.y, startPoint.y)); tempStartPoint = new Point(Math.min(endPoint.x, startPoint.x), Math.min(endPoint.y, startPoint.y)); drawWidth = tempEndPoint.x - tempStartPoint.x; drawHeight = tempEndPoint.y - tempStartPoint.y; if(shapeType == ShapeEnum.ELLIPSE) { aShape = new DecoratedEllipse(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight); } else if(shapeType == ShapeEnum.RECTANGLE) { aShape = new DecoratedRectangle(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight); } } aShape.setColor(lineColor); aShape.setStroke(drawStroke); } if (aShape != null) { if (!mousePressed) { shapes.add(aShape); ptIndex = 0; } else { aShape.paintSelf(g2dImage); } } for (int i = 0; i < shapes.size(); i++) { shapes.get(i).paintSelf(g2dImage); } } -------------------------------------------------------- また、元のpaint()メソッドのほうもずいぶん簡単なコードに変わったと思い ますが、念のために下にそのソース・コードを提示しておきます。 -------------------------------------------------------- public void paint(Graphics g) { Image image = createImage(getSize().width, getSize().height); paintImage(image); g.drawImage(image, 0, 0, getSize().width, getSize().height, null); } -------------------------------------------------------- すると、paintImage()メソッドにBufferedImageオブジェクトを引数として 渡して呼び出してやれば、描かれた絵をファイルに書き出すことが可能に なります。 では、DrawingToolFrameのほうにそのメソッドを用意しましょう。 DrawingToolFrameのソース・コードに以下のメソッドを追加してください。 -------------------------------------------------------- public void save(String fileName) { Dimension drawingSize = drawingToolPanel.getDrawingPanel().getSize(); BufferedImage rendImage = new BufferedImage(drawingSize.width, drawingSize.height, BufferedImage.TYPE_INT_RGB); drawingToolPanel.getDrawingPanel().paintImage(rendImage); File file = new File(fileName); try { ImageIO.write(rendImage, "jpg", file); } catch (IOException e) { e.printStackTrace(); } } -------------------------------------------------------- (Dimensionはjava.awtパッケージに入っています。また、 IOExceptionはjava.ioパッケージに入っています。) なお、このImageIOのwrite()メソッドでは、もし既に同名のファイルが存在 していると、それを上書きしてしまいます。 勝手に上書きされては困る場合は、ファイルが既存であるかどうかを予めチェ ックして、既存であれば上書きするかそれともキャンセルするかを聞いてくる ようにロジックを組んでおく必要があります。ファイルが既存かどうかを確認 するにはFileのexists()メソッドが使用できます。 当記事では端折りますが、余力のある人は、自分でロジックを組み込んでみて ください。 次に、DrawingToolFrameの「ファイル」メニューに保存のメニュー項目を追加 しましょう。 DrawingToolFrameのDesignビューを開いて下さい。 Paletteの「Menu」配下の「JMenuItem」を選択(クリック)し、 DrawingToolFrameのウインドウの「ファイル」メニューのところに マウス・ポインターを持っていきます。「ファイル」メニューの下に 表示される「印刷」というメニュー項目のすぐ上にマウス・ポインター を持っていくと、そこに赤い横線が表示されますので、そこでクリック してJMenuItemを貼り付けます。 変数名(Properties欄のVariable)は menuItemSave にしておきましょう。 また、textプロパティーの値を 名前をつけてJPEG保存 にしておきましょう。すると、メニュー項目に「名前をつけてJPEG保存」 という文字列が表示されますね。 では、このメニュー項目からJFileChooser(017号でお話したFileDialogの Swing版)を呼び出したあと、さきほどのsave()メソッドを呼び出すように プログラミングしたいと思います。 JFileChooserはFileDialogと同じようにファイル名を入力させたり、リスト からファイルを選択させたりするウインドウです。 このとき、JFileChooserのウインドウにリストされるファイルをJPEGのもの (拡張子が.jpgのもの)だけに絞り込むようにしておきたいので、そのため にFileFilterのサブクラスを作っておきましょう。 (JFileChooserもFileFilterもともにjavax.swing.filechooserパッケージの 中にはいっています。) FileFilterは文字通りファイルのリストをフィルタリングするためのクラス です。 このクラスは抽象クラスであり、必ずサブクラスを作ることによって使用する 必要があります。 以下のようなFileFilterのサブクラスをjp.co.flsi.lecture.cgパッケージの 中にJpgFilterというクラス名で作ってください。 -------------------------------------------------------- package jp.co.flsi.lecture.cg; import java.io.File; import javax.swing.filechooser.FileFilter; class JpgFilter extends FileFilter { public boolean accept(File f) { String filename = f.getName(); return filename.endsWith(".jpg") || f.isDirectory(); } public String getDescription() { return "*.jpg"; } } -------------------------------------------------------- ここで、accept()メソッドは引数に指定されたファイル(ディレクトリも含む) がリストに表示すべきファイルかどうかを判断するためのメソッドです。 ここでは、拡張子.jpgがついているか、またはディレクトリー(フォルダー)の 場合のみリストに表示するようにプログラミングしています。 また、getDescription()はこのフィルタリングを説明する文字列で、Windowsの ダイアログの場合は「ファイルタイプ(ファイルの種類)」欄に表示されるもの です。 では、保存のメニュー項目からJFileChooserやさきほどのsave()メソッドを呼び 出せるようにプログラミングしましょう。 再度、DrawingToolFrameのDesignビューを開いて下さい。 (1) 先ほどのDrawingToolFrameのメニュー・バーの「ファイル」配下の 「名前をつけてJPEG保存」と表示されたメニュー項目(menuItemSave)を 右クリックし、「Add event handler」→「アクション」→「actionPerformed」 を選択します。 (2) 次のコードが自動生成されましたね。 -------------------------------------------------------- menuItemSave.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { } }); -------------------------------------------------------- これを次のように編集しましょう。 -------------------------------------------------------- menuItemSave.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); fileChooser.addChoosableFileFilter(new JpgFilter()); fileChooser.showSaveDialog(menuItemSave); File file = fileChooser.getSelectedFile(); if (file != null) { String filePathName = file.getPath(); save(filePathName); } } }); -------------------------------------------------------- 上のソース・コードを簡単に解説しておきましょう。 さきほど作成したFileFilterのサブクラスは、JFileChooserの addChoosableFileFilter()メソッドを呼び出すことによって JFileChooserオブジェクトに登録します。 また、JFileChooserのウインドウのうちファイルの保存用のダイアログを表示する にはshowSaveDialog()を使用します。 また、このダイアログにファイル名が入力された、または、リストから選択された ファイルを取り出すには、getSelectedFile()メソッドを使用します。 また、FileオブジェクトのgetPath()メソッドによって、パス名(ディレクトリー名 とファイル名の組み合わせ)を取り出すことができます。 なお、FileChooserのshowSaveDialog()メソッドは、引数にfinal指定されたJMenuItem インスタンスを要求しますので、今のままではエラーになります。 そこでその上にある -------------------------------------------------------- JMenuItem menuItemSave = new JMenuItem("\u540D\u524D\u3092\u3064\u3051\u3066JPEG\u4FDD\u5B58"); -------------------------------------------------------- というコードの先頭にfinalを指定して -------------------------------------------------------- final JMenuItem menuItemSave = new JMenuItem("\u540D\u524D\u3092\u3064\u3051\u3066JPEG\u4FDD\u5B58"); -------------------------------------------------------- というふうに編集して下さい。 このfinal指定は、この変数(menuItemSave)に一度インスタンスが代入されると 以後は他のものに差し替えることができないことを意味します。 (保管のためのメニュー項目が途中で別の意味のメニュー項目に変わって しまっては困りますね。) これで、描いた絵をJPEGファイルに保存することができるようになりました。 では、次に保存したファイルを開く機能をプログラミングしましょう。 今度は -------------------------------------------------------- ImageIO.read(File inputFile) -------------------------------------------------------- というメソッドを使ってファイルから画像を読み取ります。 このreadメソッドの戻り値はBufferedImageなので、そのBufferedImageオブジェクト をGraphicsに描きだしてやる必要があります。そのためにはGraphicsのdrawImage() メソッドを使用します。 ただし、毎回毎回paint()メソッドが呼び出されるたびにdrawImage()メソッドで 描画する必要がありますから、BufferedImageオブジェクトをフィールドにして メモリー上に留めておきます。 というわけで、BufferedImageのフィールドを追加しましょう。 下記のフィールドをDrawingPanelのソース・コードに追加してください。 -------------------------------------------------------- private BufferedImage jpegImage = null; -------------------------------------------------------- (BufferedImageはjava.awt.imageパッケージに入っています。) このフィールドにアクセスするための次のsetter、getterを追加しましょう。 -------------------------------------------------------- public void setJpegImage(BufferedImage jpegImage) { this.jpegImage = jpegImage; repaint(); } public BufferedImage getJpegImage() { if (jpegImage == null) { jpegImage = new BufferedImage(getSize().width, getSize().height, BufferedImage.TYPE_INT_RGB); Graphics g = jpegImage.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, getSize().width, getSize().height); } return jpegImage; } -------------------------------------------------------- このgetter(getメソッド)では、jpegImageフィールドが空のときには真っ白に 塗りつぶした画像を入れています。これはこれから描く絵の背景になります。 (いままで絵を描いていたときには背景がなかったのです。) 逆にsetter(setメソッド)によって画像をセットし直すと、それが新しい背景に なります。 次に、DrawingPanelのpaintImage()メソッドの中の先頭の次の行、つまり -------------------------------------------------------- Graphics2D g2dImage = (Graphics2D) image.getGraphics(); -------------------------------------------------------- の行の下に -------------------------------------------------------- g2dImage.drawImage(getJpegImage(), 0, 0, getSize().width, getSize().height, null); -------------------------------------------------------- という行を追加してください。 こうすると、paintImage()メソッドの中のしょっぱなでgetJpegImage()で取り出 した画像をg2dImageに描いているわけですから、これから描く絵はその上にかぶ せられます。つまりgetJpegImage()で取り出した画像は背景になります。 では、今度はDrawingToolFrameのソース・コードのほうに、ファイルを開くための 作業を行うための次のメソッドを追加しましょう。 -------------------------------------------------------- public void load(String fileName) { try { drawingToolPanel.getDrawingPanel().setJpegImage(ImageIO.read(new File(fileName))); } catch (IOException e) { e.printStackTrace(); } } -------------------------------------------------------- 続いて、ファイルを開くためのメニュー項目を追加しましょう。 DrawingToolFrameのDesignビューを開いて下さい。 Paletteの「Menu」配下の「JMenuItem」を選択(クリック)し、 DrawingToolFrameのウインドウの「ファイル」メニューのところに マウス・ポインターを持っていきます。「ファイル」メニューの下に 表示される「名前をつけてJPEG保存」というメニュー項目のすぐ上に マウス・ポインターを持っていくと、そこに赤い横線が表示されますので、 そこでクリックしてJMenuItemを貼り付けます。 変数名(Properties欄のVariable)は menuItemOpen にしておきましょう。 また、textプロパティーの値を JPEGファイルを開く にしておきましょう。すると、メニュー項目に「JPEGファイルを開く」という 文字列が表示されますね。 (現在、上から順番に「JPEGファイルを開く」、「名前をつけてJPEG保存」、 「印刷」という3つのメニュー項目が並んでいるはずです。 もし、間違った順番に貼り付けてしまった場合は(必ずこの順番にしなければ いけない訳ではないが、順番を入れ替える方法を学ぶために)各メニュー項目 をドラッグすると順番を入れ替えられます。) では、このメニュー項目からJFileChooserやさきほどのload()メソッドを呼び 出せるようにプログラミングしましょう。 (1) 先ほどのDrawingToolFrameのメニュー・バーの「ファイル」配下の 「JPEGファイルを開く」と表示されたメニュー項目(menuItemOpen)を 右クリックし、「Add event handler」→「アクション」→「actionPerformed」 を選択します。 (2) 次のコードが自動生成されましたね。 -------------------------------------------------------- menuItemOpen.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { } }); -------------------------------------------------------- これを次のように編集しましょう。 -------------------------------------------------------- menuItemOpen.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); fileChooser.addChoosableFileFilter(new JpgFilter()); fileChooser.showOpenDialog(menuItemOpen); File file = fileChooser.getSelectedFile(); if (file != null) { String filePathName = file.getPath(); load(filePathName); } } }); -------------------------------------------------------- ここでも先ほどのFileChooserのshowSaveDialog()メソッドの場合と同じように、 FileChooserのshowOpenDialog()メソッドが、引数にfinal指定されたJMenuItem インスタンスを要求しますので、今のままではエラーになります。 そこでその上にある -------------------------------------------------------- JMenuItem menuItemOpen = new JMenuItem("JPEG\u30D5\u30A1\u30A4\u30EB\u3092\u958B\u304F"); -------------------------------------------------------- というコードの先頭にfinalを指定して -------------------------------------------------------- final JMenuItem menuItemOpen = new JMenuItem("JPEG\u30D5\u30A1\u30A4\u30EB\u3092\u958B\u304F"); -------------------------------------------------------- というふうに編集して下さい。 以上で、ファイルに保存したりファイルを開いたりする機能が完成しました。 ところで、これまでに見てきたSwingのルック&フィール(Look & Feel:036号参照) はJava独自のメタル・ルック&フィールと呼ばれるもの(これがデフォルト)です。 これはWindowsのルック&フィールとは異なるので、多くの人にはちょっと馴染み にくいかもしれません。 その場合は、DrawingToolFrame()コンストラクターの最後などに -------------------------------------------------------- try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); SwingUtilities.updateComponentTreeUI(this); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (UnsupportedLookAndFeelException e) { e.printStackTrace(); } -------------------------------------------------------- というコードを加えてみてください。ルック&フィールがその環境のもの (Windowsの環境ならWindowsのルック&フィール)に変わります。 (なお、UIManagerとSwingUtilitiesとUnsupportedLookAndFeelExceptionは javax.swingパッケージに入っています。) 念のため、このコードを加えたDrawingToolFrame()コンストラクター全体の ソース・コードを下記に提示しておきます。 -------------------------------------------------------- public DrawingToolFrame() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 450, 300); JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); JMenu menuFile = new JMenu("\u30D5\u30A1\u30A4\u30EB"); menuBar.add(menuFile); JMenuItem menuItemPrint = new JMenuItem("\u5370\u5237"); menuItemPrint.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { printDrawing(); } }); final JMenuItem menuItemSave = new JMenuItem("\u540D\u524D\u3092\u3064\u3051\u3066JPEG\u4FDD\u5B58"); menuItemSave.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); fileChooser.addChoosableFileFilter(new JpgFilter()); fileChooser.showSaveDialog(menuItemSave); File file = fileChooser.getSelectedFile(); if (file != null) { String filePathName = file.getPath(); save(filePathName); } } }); final JMenuItem menuItemOpen = new JMenuItem("JPEG\u30D5\u30A1\u30A4\u30EB\u3092\u958B\u304F"); menuItemOpen.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); fileChooser.addChoosableFileFilter(new JpgFilter()); fileChooser.showOpenDialog(menuItemOpen); File file = fileChooser.getSelectedFile(); if (file != null) { String filePathName = file.getPath(); load(filePathName); } } }); menuFile.add(menuItemOpen); menuFile.add(menuItemSave); menuFile.add(menuItemPrint); contentPane = new JPanel(); contentPane.setBorder(new EmptyBorder(5, 5, 5, 5)); contentPane.setLayout(new BorderLayout(0, 0)); setContentPane(contentPane); drawingToolPanel = new DrawingToolPanel(); contentPane.add(drawingToolPanel, BorderLayout.CENTER); setSize(600, 500); try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); SwingUtilities.updateComponentTreeUI(this); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (UnsupportedLookAndFeelException e) { e.printStackTrace(); } } -------------------------------------------------------- では、保管したあと、DrawingToolFrameを起動してテストしてみましょう。 絵を描いてファイルへ保存したり、保存したファイルを開いたりしてみてください。 なお、ファイルを保存するときは、ファイル名には必ず.jpgという拡張子をつけて ください。 どうですか。うまくいきましたか。 テストしてみると分かるように、Windowsのルック&フィールではトグル・ボタン に色が付かないなどの欠点がありますので、気に入らない人はデフォルトの ルック&フィールに戻しましょう。 何か、わからないところがありましたら、下記のWebページまで質問をお寄せ ください。 今回はここまで。 (続く) 以上、今回は ┌───────────────────────────┐ ・ラスター・グラフィックスのファイルの保存の仕方と開き方 (特にJPEGに保存する方法と、ファイルを開いて画像表示する方法) ・JFileChooserとFileFilterの使い方 ・メニュー項目の順番の並べ替え方 ・Swingのルック&フィールを環境に合わせる方法 └───────────────────────────┘ などを学習しました。 では、今日はここまでにします。 ======================================================== ◆ 02.Java(文法等)解説 [Serializable] ======================================================== Javaのプログラム(Javaだけでなく通常のプログラム)は、実行が終わると メモリー上から消えます。 したがって、プログラムの実行時にメモリー上に存在していたオブジェクト もメモリー上から消えます。 これに対して、実行時のオブジェクトの状態をそのままハードディスクなどに 保管しておいて、次回のプログラムの実行時に復元して再使用したいという場合が ありますが、そのためにJavaでは直列化(serialization)という機能が提供され ています。 この直列化の機能を利用するためにはSerializableというインターフェース(または そのサブインターフェース)を実装(implements)する必要があります。 この直列化(serialization)の機能は、元々JavaBeans技術の開発と同時期に 作られた機能であり、JavaBeansと深い関係があります。 たとえば、ビルダー・ツールでBeanをビジュアルに貼り付けていって、各Beanの プロパティーを画面上で設定したときに、そのBeanの状態を保管し、次回のビルダー・ ツールの起動時にBeanの状態を再現するためなどに使われていました。 (したがって、AWTやSwingのGUI部品などのビジュアルなクラス群は予めSerializable が実装されています。) したがって、BeanはSerializableを実装することが推奨されています。 (よく誤解している人がいるようなので特記しましたが、Serializableを 実装することはBeanにとって必須ではなく、あくまで推奨です。 とはいっても、直列化の機能を利用したい場合は、Serializable(またはその サブインターフェース)の実装は必須になります。) 現在ではビルダー・ツールはほとんど使われていないので、上記の話は重要ではないの ですが、しかしながら直列化(serialization)の機能は重宝で今でも(今後も)よく 使われています。 ところで、実装するという言い方をしましたが、Serializableは他の普通のインター フェースと異なり、実装すべきメソッドはありません。フィールドもありません。 Serializableは、たんに「直列化(serialization)が可能である」とか「直列化して いいよ」という意味を表すだけの特殊なインターフェースです。 したがって、たんにクラスの定義に implements Serializable と書けばいいだけなので使い方はとても簡単です。 さて、この直列化(serialization)の機能を使うと、オブジェクト(インスタンス)を そのままファイルに保管することができます(あるいはネットワークを通して他のコン ピューターに送信することもできます)。 ただし、そのままと言ってもメソッドのロジックまでも保存されるわけではなく、その 時点のインスタンスの状態(つまり各属性(フィールド)の値)だけが保存されます。 この機能がどうして直列化(serialization)と呼ばれるのか疑問に思う人もいるでしょう。 直列化という翻訳はおそらく電子回路の素子や電池などの「直列接続(serial connection)」 という言葉を参考にして作られたものと思われます。(ちなみに「並列接続」は英語で parallel connectionと言う。) しかし、この翻訳では、どういう意味だかさっぱりわからないでしょう。 そこで、このserializationという用語の成り立ちからお話しましょう。 ことの始めは、順次アクセス(serial access)という言葉からスタートします。 コンピューターの内部(主にメモリー)に書かれたデータは、いつでも好きな場所 (アドレス)に直接読み書きができます。 ところが、コンピューターの外部へデータを保管しようと思ったら、昔は磁気テープの ような順次アクセス(serial access)をする媒体を使うしかなかったのです。 順次アクセス(serial access)というのは、前から順番に読み書きしていかなけれ ばならないアクセスの仕方をいい、たとえ途中に必要なデータがあったとしても 直接そこに読み書きに行くことはできず、必ず前から順番に読み取って行ってそこに 到達しなければなりません。 ┌補足─────────────────────────┐ serialとは「連続的な」とか「順次の」というような意味を表す 形容詞で、何かが前から順番に続いているようなイメージを持つ 言葉です。 たとえば、serial numberというと、通し番号とか連番というよう な意味になります。 └───────────────────────────┘ したがって、メモリー上のデータを外部記憶装置に保管したいと思ったら、データ構造 を順次アクセス用のデータ構造に変換してやる必要があったのですが、このような変換は serializeと呼ぶようになりました。 英語には、「・・化する」という意味を持つ"ize"という接尾辞があるので、serial化する と言いたいときは、自然とserializeという動詞が使われるようになったのです。 (serializeという動詞は以前から存在していましたが、以前は「連載する」という ような意味で使われており、コンピューターとは関係ありませんでした。 したがって、serializeという言葉がコンピューター用に別の意味に転用されたことに なります。) さらには、順次化というような意味の名詞を使いたいときには、serializeのうしろに 名詞化の接尾辞"ation"をつけて、serializationという言葉にして使うようになりま した。 また、「serializeが可能である」という状態を表す形容詞を使いたいときには、 serializeのうしろに可能の意味を持つ形容詞化の接尾辞"able"をつけて、serializable という言葉にして使うようになりました。 したがって、この直列化(serialization)は、元々は、オブジェクト(インスタンス) を順次アクセスのデータ構造に変換することを意味しているのです。 インスタンスの状態を保管したいときは、順次アクセスのデータ構造にしておけば、 ハードディスクであろうと、磁気テープであろうとどんな記憶媒体にも保管できます。 あるいは、ネットワークを通して他のコンピューターに送付することもできます。 (ネットワークを通して送付するときは時系列的に順次に送るしかないので必然的に 順次アクセスになる。) というわけで、インスタンスの状態をファイルに保管したり、ネットワークを通して 送付するための機能をserializationと呼ぶようになっています。 ところで、serializationは、あくまでインスタンスの状態を外部に書き出す機能です。 状態を書き出すとは、具体的にはインスタンスの属性の値、つまりフィールドの値を 書き出すものであり、メソッドは書き出されません。 また、staticやtransient(後述)が指定されているフィールドの値は書き出されませ ん。これは、staticはインスタンスには属さないもの(いわゆるクラス変数)であり、 transientは一時的なものだからです。 直列化(serialization)はインスタンスのフィールドでかつ永続的なもの(一時的では ないもの)しか保管しません。 直列化(serialization)によるインスタンスの状態の書き出し/読み込みは直列化の メソッド(ObjectOutputStreamのwriteObject()メソッド/readObject()メソッド)を 呼び出すことにより実行することができます。 このメソッドが具体的に何をどんな順番でファイルに書き出すのかについては、直列化 (serialization)の仕様書に書かれていますが、プログラミングのために細かい仕様 を知る必要はありません。 特定のフィールドの値を保管しないなど、インスタンスの状態の書き出し/読み込みに 独自の制御を行いたい場合は、Serializableの代わりにそのサブインターフェースであ るExternalizableというインターフェースを実装することによって、独自にプログラミ ングを行うことができます。 ところで、直列化(serialization)で保管したインスタンスを次回読み込んで使おう としたら、その時点ではそのクラスに改良が加えられていて、保管しておいたインス タンスとちぐはぐが生じていた、なんてことになると困りますね。 インスタンスの状態が正しく読み込めなくてエラーになるのはまだいいほうで、一番 困るのは読み込みは正常に行えてしまうが、プログラムが誤動作を起し、しかもその 誤動作に気がつかずにそのままプログラムを続行させてしまう場合です。 このような問題に対処するために、直列化(serialization)にはクラスのバージョン をチェックする機能が備えられています。 この機能により、インスタンスの状態を保管(ファイルに書き出し)したときには、 そのクラスのバージョンID(バージョン番号)がいっしょにファイルに書き込まれます。 そして、そのファイルからインスタンスを復元(インスタンスの状態を読み出し)する ときに、読み出しを行うプログラムの中の該当するクラスのバージョンIDとファイルの中 のバージョンIDが照合されます。 そして、それらのバージョンIDに食い違いがあるときにはエラー(InvalidClassException) を返すようになっています。 このバージョンIDは、クラスにserialVersionUIDというフィールド、具体的には private static final long serialVersionUID = 1L; というような指定(privateの代わりにpublicでも何でもかまわないが通常はprivate にする)でlong型のフィールドを定義することによって設定できます。 このserialVersionUIDは明示的に指定しなくてもコンパイラーが自動的に生成して くれますが、明示的に指定されることが推奨されます。 そのため、Serializableを実装している(あるいはスーパークラスがSerializableを実装 している)クラスにおいてserialVersionUIDを明示的に指定していないと、コンパイラー が警告を返します。 Eclipseのエディターではこのとき警告の橙色のマークがつきます。そこで、橙色の下線 が引かれているクラス名にカーソルを入れてCtrl+1キーを押す(Ctrlキーと数字の1のキー を同時に押す)と、対処法のリストが出ますので、その中から「デフォルト・シリアル バージョンIDの追加」を選択すると private static final long serialVersionUID = 1L; という行が自動的に挿入されます。この機能を使うと、serialVersionUIDの指定は 簡単に行うことができますが、実際の業務で使用するプログラムではこの1Lという 番号は、クラスのバージョンに合わせて変える(バージョンIDをきちんと取り決める) べきです。) 直列化について、さらに詳しく知りたい人は、 http://java.sun.com/javase/ja/6/docs/ja/platform/serialization/spec/serial-arch.html などを参照して下さい。 では、Eclipseで直列化の機能を使ったプログラムを書いてみましょう。 まず、外部ファイルに保管するオブジェクトのクラスを以下の2種類作成しましょう。 -------------------------------------------------------- package jp.co.flsi.test.serialization; import java.io.Serializable; public class Student implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; private transient boolean truant; // 授業をさぼっているかどうか private static int numberOfStudents = 0; public Student() { setName("無名"); setAge(0); setTruant(false); } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public void setTruant(boolean truant) { this.truant = truant; } public boolean isTruant() { return truant; } public static void setNumberOfStudents(int numberOfStudents) { Student.numberOfStudents = numberOfStudents; } public static int getNumberOfStudents() { return numberOfStudents; } } -------------------------------------------------------- -------------------------------------------------------- package jp.co.flsi.test.serialization; import java.io.Serializable; public class Teacher implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; private String subject; public Teacher() { setName("無名"); setAge(0); setSubject("適当な科目"); } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public void setSubject(String subject) { this.subject = subject; } public String getSubject() { return subject; } } -------------------------------------------------------- これらのクラスのインスタンスを生成し、それを外部のファイル(ここではファイル名を "SerialData.test"とする)に保管するプログラムを下記のように作成しましょう。 -------------------------------------------------------- package jp.co.flsi.test.serialization; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class SerialWriter { public static void main(String[] args) { Student tarou = new Student(); tarou.setName("太郎"); tarou.setAge(16); tarou.setTruant(true); tarou.setNumberOfStudents(tarou.getNumberOfStudents() + 1); Student hanako = new Student(); hanako.setName("花子"); hanako.setAge(17); hanako.setNumberOfStudents(hanako.getNumberOfStudents() + 1); Teacher hanawa = new Teacher(); hanawa.setName("塙"); hanawa.setAge(35); hanawa.setSubject("数学"); try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("SerialData.test")); out.writeObject(tarou); out.writeObject(hanako); out.writeObject(hanawa); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println(tarou.getName()); System.out.println(tarou.getAge()); System.out.println(tarou.isTruant()); System.out.println(tarou.getNumberOfStudents()); System.out.println(hanako.getName()); System.out.println(hanako.getAge()); System.out.println(hanako.isTruant()); System.out.println(hanako.getNumberOfStudents()); System.out.println(hanawa.getName()); System.out.println(hanawa.getAge()); System.out.println(hanawa.getSubject()); } } -------------------------------------------------------- 続いて、"SerialData.test"ファイルから保管されたオブジェクトを読み出して インスタンスを復元するプログラムを以下のように作成しましょう。 -------------------------------------------------------- package jp.co.flsi.test.serialization; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; public class SerialReader { public static void main(String[] args) { Student tarou = new Student(); Student hanako = new Student(); Teacher hanawa = new Teacher(); try { ObjectInputStream in = new ObjectInputStream(new FileInputStream("SerialData.test")); tarou = (Student)in.readObject(); hanako = (Student)in.readObject(); hanawa = (Teacher)in.readObject(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println(tarou.getName()); System.out.println(tarou.getAge()); System.out.println(tarou.isTruant()); System.out.println(tarou.getNumberOfStudents()); System.out.println(hanako.getName()); System.out.println(hanako.getAge()); System.out.println(hanako.isTruant()); System.out.println(hanako.getNumberOfStudents()); System.out.println(hanawa.getName()); System.out.println(hanawa.getAge()); System.out.println(hanawa.getSubject()); } } -------------------------------------------------------- 各オブジェクトを保管した順番でreadObject()していることに注意して下さい。 このようにちゃんと順次に処理していく必要があります。 完成した各ソース・コードを保管したら、まずSerialWriterを実行してみて 下さい。 各インスタンスがファイルに保管されるとともにそのインスタンスの 各フィールドの値がコンソールに出力されますね。 次にSerialReaderを実行してみて下さい。 各インスタンスがファイルから復元されるとともにそのインスタンスの 各フィールドの値がコンソールに出力されますね。 このときtransientを指定したフィールドに注目してください。 SerialWriterの実行時にはtrueに設定しておいたはずのフィールドも、 SerialReaderを実行してみると、初期値のfalseに変わってしまって いますね。 staticフィールドも同様に初期値の0に変わってしまっていますね。 (続く) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ★ホームページ: http://www.flsi.co.jp/Java_text/ ★このメールマガジンは 「まぐまぐ(http://www.mag2.com)」 を利用して発行しています。 ★バックナンバーは http://www.flsi.co.jp/Java_text/ にあります。 ★このメールマガジンの登録/解除は下記Webページでできます。 http://www.mag2.com/m/0000193915.html ★このメールマガジンへの質問は下記Webページにて受け付けて います。わからない所がありましたら、どしどしと質問をお寄 せください。 http://www.flsi.co.jp/Java_text/ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Copyright (C) 2012 Future Lifestyle Inc. 不許無断複製 |