ファイルをバイナリ形式で表示する (Java+Swing編)
ファイルデータ・ビューでは以下のイベントを処理します。
描画イベント。
垂直スクロールバーの操作イベント。
水平スクロールバーの操作イベント。
マウスホイールの操作イベント。
マウスの左ボタンを押下したときの操作イベント。
マウスホイールの操作イベント。
ウインドウサイズの変更イベント。
表示するファイルデータを読み込みます。
ファイルサイズを取得します。
ファイルサイズが読み込み上限(10MB)を超えていた場合は読み込むサイズを読み込み上限値にします。
ファイルの読み込み用バッファを確保します。
ファイルを読み込みモードでオープンします。
確保した読み込み用バッファにファイルの内容を読み込みます。
(読み込むデータサイズ分のメモリは確保してあるので一気に読み込みます。)ファイルをクローズして終了します。
スクロールバーを読み込んだファイルサイズに合せて初期化します。
最後にバイナリ表示ビューを再描画します。
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  ファイルを読み込んでバイナリ形式で表示する.
 *
 *  @param  path    バイナリ表示するファイルの絶対パス.
 */
public void showBinaryFile( String path ) {
    fileName = path;
    try {
        File file = new File( fileName );
        //! ファイルのバイト数を取得する.
        fileSize = (int)file.length();
        /*!
         * ファイルサイズが読み込みサイズの上限を超える場合は
         * サイズの上限までしか読み込まない.
         */
        if( fileSize > MAX_FILE_SIZE ){
            fileSize = MAX_FILE_SIZE;
        }
        if( fileSize > 0 ){
            //! ファイルサイズ分のファイルの読み込み用バッファを確保する.
            fileData = new byte[fileSize];
            //! ファイルを読み込みモードでオープンする.
            FileInputStream     fs = new FileInputStream( fileName );
            BufferedInputStream bs = new BufferedInputStream( fs );
            DataInputStream     ds = new DataInputStream( bs );
            //! ファイルを読み込み用バッファに1回で読み込む.
            int readByte = ds.read( fileData );
            //! ファイルをクローズする.
            ds.close();
            //! 垂直スクロールバーを初期化する.
            scrollV.setMinimum( 0 );
            scrollV.setMaximum( (readByte + 15) / 16 );
            scrollV.setValue( 0 );
            //! 水平スクロールバーを初期化する.
            scrollH.setMinimum( 0 );
            scrollH.setMaximum( 74 );
            scrollH.setValue( 0 );
            //! 画面に合わせてスクロールバーのつまみ幅を設定する.
            setScrollBar();
            //! バイナリ表示ビューを再描画する.
            repaint();
        }
    } catch( FileNotFoundException e ) {
        fileSize = 0;
    } catch( IOException e ) {
        fileSize = 0;
    }
}
	
	ファイルデータの描画は以下の手順で行います。
- 
	
以下の固定幅フォントを作成します。
項目 値 フォント名 "MS ゴシック" スタイル Font.PLAIN フォントサイズ 14  作成したフォントの幅と高さを取得します。
- 
	
描画開始位置を求めます。
縦方向の描画開始座標は、フォントのベースラインの位置にします。
(Graphics2D.drawString()で指定するY座標はフォントのトップ位置ではなくてベースライン位置の為です。)横方向の描画開始座標を水平スクロールバーの現在の値から算出します。
(横方向の描画開始座標) = (水平スクロールバーのつまみ位置) × (フォントの幅) × -1
水平スクロールバーのつまみが右に移動するとビューの画像は左に移動することになるので、オフセットに-1をかけます。
 最初にスケールを描画します。
- 
	
次にファイルデータを描画します。
1画面に描画可能な行数を画面の高さとフォントの高さから算出します。
(描画可能な行数) = (画面の高さ + (フォントの高さ - 1)) ÷ (フォントの高さ)
画面の高さがフォントの高さで割り切れない場合に(画面の高さ)÷(フォントの高さ)では端数の領域が空白になってしまうので、(フォントの高さ-1)だけ画面の高さを割り増しして端数の領域も1行分として増やして文字を描画するようにして、空白にならないようにします。
スケールの1行を除いた1画面に描画可能な行数分のファイルデータを1行づつバイナリ形式で描画します。
 
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  バイナリ表示ビューを描画する.
 *
 *  @param  g   グラフィック・コンテキスト.
 */
@Override
public void paintComponent( Graphics g ) {
    super.paintComponent( g );
    Graphics2D g2d = (Graphics2D)g;
    //! 固定幅フォントを使用する.
    Font font = new Font( "MS ゴシック", Font.PLAIN, 14 );
    g2d.setFont( font );
    //! フォントサイズを取得する.
    FontMetrics fm = g2d.getFontMetrics();
    int fontW = fm.charWidth( '0' );
    int fontH = fm.getHeight();
    //! 表示領域のサイズを取得する.
    int viewH = getHeight();
    //! 描画開始座標を取得する.
    int y = fm.getAscent();
    int x = -1 * scrollH.getValue();
    //! スケールを描画する.
    drawScale( g2d, x, y, fontW, fontH );
    //! 描画開始Y座標を更新する.
    y += fontH;
    if( fileSize > 0 ){
        //! 1画面に描画可能な行数を計算する.
        int lines = (viewH + (fontH - 1)) / fontH;
        //! 描画する行のオフセット・アドレスを計算する.
        int offset = scrollV.getValue() * 16;
        //! ファイルデータをバイナリ形式で描画する.
        for( int i = 1; i < lines; i++ ){
            if( offset >= fileSize ){
                break;
            }
            //! 1行分のバイナリデータを描画する.
            drawBinaryData( g2d, x, y, fontW, fontH, i, offset );
            offset += 16;
            //! 描画開始Y座標を更新する.
            y += fontH;
        }
    }
}
	
	バイナリデータが見易くなるようにスケールを描画します。
垂直方向にスクロールしてもスケールは常に表示するようにしますが、水平方向のスクロールした場合はスケールも移動するようにします。
オフセットアドレスを描画する領域の背景を塗りつぶしておきます。
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  スケールを描画する.
 *
 *  @param  g2d     グラフィック・コンテキスト.
 *  @param  x       描画開始X座標.
 *  @param  y       描画開始Y座標.
 *  @param  fontW   フォントの幅.
 *  @param  fontH   フォントの高さ.
 */
private void drawScale( Graphics2D g2d, int x, int y, int fontW, int fontH ) {
    //! 表示領域のサイズを取得する.
    int w = getWidth();
    int h = getHeight();
    g2d.setColor( new Color(128, 128, 128) );
    g2d.fillRect( x * fontW, 0, w - (x * fontW), fontH );
    g2d.fillRect( x * fontW, 0, fontW * 9, h );
    //! バイナリ表示ビューのスケールを描画する.
    g2d.setColor( Color.white );
    String str1 = "ADDRESS:";
    g2d.drawString( str1, x * fontW, y );
    String str2 = "+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F";
    g2d.drawString( str2, (x + 10) * fontW, y );
    String str3 = "ASCII";
    g2d.drawString( str3, (x + 58) * fontW, y );
}
	
	1行分のファイルデータの表示を行います。
- 
	
最初に描画する行のファイル上のオフセット・アドレスを描画します。
オフセット・アドレスは16進数で8桁固定で描画するので、オフセット・アドレスを16進数8桁の文字列に変換して描画します。
 - 
	
次にファイルデータを16バイト(1行分)描画します。
まず、描画するバイト数を求めます。
今回描画するオフセット・アドレスに16を加算した値がファイルサイズを超えてしまう場合、ファイルサイズ - オフセット・アドレス を描画するバイト数とします。
ファイルサイズを超えない場合は16バイト分描画します。
forループで以下の処理を複数回実行します。
ファイルデータは16進数で2桁固定で描画するので、ファイルデータを16進数2桁の文字列に変換してバイナリデータの描画用文字列に追加します。
ファイルデータをASCII文字に変換して、描画用のASCII文字列も併せて作ります。
(ファイルデータが0x20以上0x7F以下の場合は該当するASCII文字にし、それ以外は「.」に変換します。)最後にforループ内で作成しておいたバイナリデータの描画用文字列とASCII文字列を描画します。
 
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  1行分のバイナリデータを描画する.
 *
 *  @param  g2d     グラフィック・コンテキスト.
 *  @param  x       描画開始X座標.
 *  @param  y       描画開始Y座標.
 *  @param  fontW   フォントの幅.
 *  @param  fontH   フォントの高さ.
 *  @param  line    描画開始する行位置.
 *  @param  offset  描画開始するバイナリデータのアドレス.
 */
private void drawBinaryData( Graphics2D g2d, int x, int y, int fontW, int fontH, int line, int offset ) {
    //! この行のデータの先頭アドレスを描画する.
    String adr = String.format( "%08X", offset );
    g2d.setColor( Color.white );
    g2d.drawString( adr, x * fontW, y );
    //! この行で描画するバイト数を求める.
    int cnt = ((offset + 16) > fileSize ? (fileSize - offset) : 16);
    //! 1行分の表示データを生成する.
    String data  = new String();
    String ascii = new String();
    for( int i = 0; i < cnt; i++ ){
        int c = fileData[offset + i] & 0xFF;
        data +=  String.format( "%02X", c );
        data += " ";
        if( 0x20 <= c && c < 0x80 ){
            ascii += (char)c;
        }else{
            ascii += '.';
        }
    }
    //! バイナリデータを描画する.
    g2d.setColor( Color.black );
    g2d.drawString( data, (x + 10) * fontW, y );
    //! バイナリデータに対応したACSII文字列を描画する.
    g2d.drawString( ascii, (x + 58) * fontW, y );
}
	
	ウインドウサイズ等が変更されると、スクロールバーのページサイズの変更が必要になります。
最初にファイルデータ・ビューの表示領域のサイズを取得します。
描画に使用するフォントを作成します。
次に表示領域のサイズとフォントの高さから1ページに表示可能な行数を計算します。
次に表示領域のサイズとフォントの幅から1ページに表示可能な文字数を計算します。
垂直スクロールバーの1ページの移動量を1画面あたりの描画行数の1/2にします。
水平スクロールバーの1ページの移動量を1画面あたりの描画文字数の1/2にします。
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  スクロールバーを設定する.
 *
 */
private void setScrollBar() {
    //! クライアント領域のサイズを取得する.
    int viewW = getWidth();
    int viewH = getHeight();
    //! グラフィックス・オブジェクトを取得する.
    Graphics2D g2d = (Graphics2D)this.getGraphics();
    //! 固定幅フォントを作成する.
    Font font = new Font( "MS ゴシック", Font.PLAIN, 14 );
    g2d.setFont( font );
    //! フォントサイズを取得する.
    FontMetrics fm = g2d.getFontMetrics();
    int fontW = fm.charWidth( '0' );
    int fontH = fm.getHeight();
    //! 1画面に表示可能な行数を計算する.
    int lines = (viewH + (fontH - 1)) / fontH;
    //! 1画面に表示可能な文字数を計算する.
    int colomuns = (viewW + (fontW - 1)) / fontW;
    //! スクロールバーのつまみ幅を画面に合わせて再設定する.
    scrollV.setVisibleAmount( lines    / 2 );
    scrollH.setVisibleAmount( colomuns / 2 );
    //! グラフィックス・オブジェクトを解放する.
    g2d.dispose();
}
	
	FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  スクロールバーを操作したときのイベント処理.
 *
 *  @param  e   イベント・パラメータ.
 */
public void adjustmentValueChanged( AdjustmentEvent e ) {
    //! バイナリ表示ビューを再描画する.
    repaint();
}
	
	スクロールバーの操作イベントです。
バイナリ表示ビューを再描画します。
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  マウスホイールを操作したときのイベント処理.
 *
 *  @param  e   イベント・パラメータ.
 */
private void onMouseWheel( MouseWheelEvent e ){
    int val = scrollV.getValue();
    if( e.getWheelRotation() > 0 ){
        val += e.getWheelRotation();
        if( val > scrollV.getMaximum() ){
            val = scrollV.getMaximum();
        }
        scrollV.setValue( val );
    }else
    if( e.getWheelRotation() < 0 ){
        val += e.getWheelRotation();
        if( val < scrollV.getMinimum() ){
            val = scrollV.getMinimum();
        }
        scrollV.setValue( val );
    }
    //! バイナリ表示ビューを再描画する.
    repaint();
}
	
	マウスホイールの操作イベントです。
MouseWheelEvent.getWheelRotation()を呼び出してマウスホイールの移動量を取得します。
マウスホイールの移動量が正数の場合、スクロールバーの下矢印ボタンを押したときと同じ動作、マウスホイールの移動量が負数の場合、スクロールバーの上矢印ボタンを押したときと同じ動作になるようにします。
マウスの現在のポジションにマウスホイールの移動量を加算して移動後のマウスポジションを計算します。このとき、スクロールバーの最小値と最大値を超えないように補正します。
計算したマウスポジションを垂直スクロールバーにセットします。
最後にバイナリ表示ビューを再描画します。
ウインドウサイズが変更されると、ビューの描画できる領域が変わるので、スクロールバーのつまみの長さをビューの描画領域に合わせて変更します。
	その為、イベントが発生したときにスクロールバーの再設定を行います。
FileDataView.java
/* ----------------------------------------------------------------------- */
/**
 *  ウインドウサイズを変更したときのイベント処理.
 *
 *  @param  e   イベント・パラメータ.
 */
private void onResized( ComponentEvent e ) {
    //! スクロールバーを設定する.
    setScrollBar();
}