DOBON.NET DOBON.NETプログラミング掲示板過去ログ

スクロールバーをつけて画像を表示しながら、その画像を拡大縮小できるガイド枠を描画しいたい

環境/言語:[言語 : C# / .NET Framework : 3.5 / OS:XP(SP3), Vista(SP1), Win7(SP1)]
分類:[.NET]

2012/02/28(Tue) 21:14:26 編集(投稿者)
2012/02/28(Tue) 21:14:15 編集(投稿者)

【解決したい問題】
「スクロールバーを付けて画像を表示する」
http://dobon.net/vb/dotnet/graphics/scrollimage.html
の方式で画像を表示しましたが、
マウスのドラッグ操作で、画像表示をリサイズするためのガイド枠を表示したいです。
そのために、
- Mouseイベントはどのコントロール(Panel?PictureBox?両方?)で拾うべきか?
- 枠(Graphics.DrawRectangle() を使用することを想定しています)はどのイベントで描画するべきか?
についてご教示いただきたいです。
そもそも、元のページの方式ではやりたいことが実現できないということであれば、そのような指摘でも構いません。

【解決するために何をしたか】
- MouseDownイベントで、枠線を描画するようにしましたが、Panelでイベントを拾った場合、画像の上(PictureBox内)ではハンドルできませんでした
- PictureBox.Imageへ枠線を描画しましたが、拡大時(PictureBoxの外側へマウスを移動)には描画できませんでした(当たり前)
- Panel.Paintで、Panelに対して枠線を描画してみましたが、PictureBoxの下に描画されてしまっているようで、見えませんでした。

【追加事項】
- Panelに描画したものを、PictureBoxの上へ表示(背景の透過率100%とかで)することはできないものでしょうか?

2012/02/28 21:13修正しました
2012/02/27(Mon) 10:42:05 編集(投稿者)

■No29949に返信(ryoさんの記事)

> マウスのD&D操作で、画像表示をリサイズするためのガイド枠を表示したいです。
ドロップ対象がない操作のようなのでドラッグアンドドロップではなくドラッグですね?


> - Panelに描画したものを、PictureBoxの上へ表示(背景の透過率100%とかで)することはできないものでしょうか?
両方に描画すればいいんじゃないかな?
■No29949に返信(ryoさんの記事)
> マウスのD&D操作で、画像表示をリサイズするためのガイド枠を表示したいです。
フォームデザイナにボタン等を貼ったときや、Excel のオートシェイプなどのように、
コンテンツ周辺にドラッグ用のハンドルを設ける形でしょうか。それとも、
矩形の対角点2箇所の範囲をドラッグ指定することでサイズを指定するイメージでしょうか。

後者だとしたら、たとえば拡大時は、元画像の外側までドラッグすることで
『拡大後のサイズとなる領域を指定する』パターンもあれば、その逆に、
元画像の内側の一部をクリッピングするようにドラッグ指定して
『画像のこの範囲が画面一杯に広がるように指定する』パターンも考えられます。

また、拡大指定と縮小指定はどのように使い分けるのか、特に、
スクロール範囲外どころか、画面サイズさえも超えた範囲指定はどうするのか、など。


> - Mouseイベントはどのコントロール(Panel?PictureBox?両方?)で拾うべきか?
PictureBox 全体に画像が表示されるのが、PictureBox の一部領域だけに表示されるのかにもよりますが、
ドラッグ開始点が PictureBox の上にあるなら PictureBox、
ドラッグ開始点が Panel の上にあるなら Panel、
ドラッグ開始点が Form の上にあるなら Form という感じではないかと。


> - 枠(Graphics.DrawRectangle() を使用することを想定しています)はどのイベントで描画するべきか?
マウス系イベントで元画像に描画するか、もしくは Paint イベントで前景描画するか、ですかね。
私なら、BackgroundImage に画像をセットしておいた上で、矩形は Paint で描画させると思います。


> - PictureBox.Imageへ枠線を描画しましたが、拡大時(PictureBoxの外側へマウスを移動)には描画できませんでした(当たり前)
その方法の場合、「描画結果も合わせた矩形サイズ」の Bitmap を
Image に再割り当てしてから描画しないと駄目でしょうね。
元画像が 300x400 で、ドラッグ指定がその中の (50, -50)-(350, 500) だとしたら、
描画結果の画像は最低でも 350x550 のキャンパスが必要になる計算です。


> - Panelに描画したものを、PictureBoxの上へ表示(背景の透過率100%とかで)することはできないものでしょうか?
Panel で無くとも良いのなら、Microsoft.Ink.InkOvelay をフォームにアタッチさせるとか。
Region で矩形に切り取った別ウィンドウを重ねて表現するとか。
GetDC(Intptr.Zero) に対して、InvertRect 等で描画するとか。
■No29951に返信(shuさんの記事)
> 2012/02/27(Mon) 10:42:05 編集(投稿者)
>
> ■No29949に返信(ryoさんの記事)
>
>>マウスのD&D操作で、画像表示をリサイズするためのガイド枠を表示したいです。
> ドロップ対象がない操作のようなのでドラッグアンドドロップではなくドラッグですね?
ご指摘のとおりです。
投稿を編集しました。

>
>
>>- Panelに描画したものを、PictureBoxの上へ表示(背景の透過率100%とかで)することはできないものでしょうか?
> 両方に描画すればいいんじゃないかな?
>
・PictureBoxに描画されるものは、PictureBoxへ描画。
・その外側になってしまうものはPanelへ描画
というような感じでしょうか?
ご教示いただいた内容は、DrawLineで各線を描画するイメージでよいでしょうか?
■No29952に返信(魔界の仮面弁士さんの記事)
> ■No29949に返信(ryoさんの記事)
>>マウスのD&D操作で、画像表示をリサイズするためのガイド枠を表示したいです。
> フォームデザイナにボタン等を貼ったときや、Excel のオートシェイプなどのように、
> コンテンツ周辺にドラッグ用のハンドルを設ける形でしょうか。それとも、
> 矩形の対角点2箇所の範囲をドラッグ指定することでサイズを指定するイメージでしょうか。
>
最終的なイメージとしては前者ですが、とりあえずは右端をドラッグして左右に動かす
&それに連動して枠が出る(右端の位置が動くだけ)でよいかなと考えています。


> 後者だとしたら、たとえば拡大時は、元画像の外側までドラッグすることで
> 『拡大後のサイズとなる領域を指定する』パターンもあれば、その逆に、
> 元画像の内側の一部をクリッピングするようにドラッグ指定して
> 『画像のこの範囲が画面一杯に広がるように指定する』パターンも考えられます。
>
”拡大後のサイズとなる領域を指定する”パターンになります

> また、拡大指定と縮小指定はどのように使い分けるのか、特に、
> スクロール範囲外どころか、画面サイズさえも超えた範囲指定はどうするのか、など。
>
縮小:限界サイズを決める
拡大:画面いっぱいまで
で考えています。

>
>>- Mouseイベントはどのコントロール(Panel?PictureBox?両方?)で拾うべきか?
> PictureBox 全体に画像が表示されるのが、PictureBox の一部領域だけに表示されるのかにもよりますが、
> ドラッグ開始点が PictureBox の上にあるなら PictureBox、
> ドラッグ開始点が Panel の上にあるなら Panel、
> ドラッグ開始点が Form の上にあるなら Form という感じではないかと。
>
つまり、すべてで同様に拾う必要があるということですね。
なお、Panel.Dock = Fillとしているので、Formで拾う必要は無いかと思いますが合っていますか?

>
>>- 枠(Graphics.DrawRectangle() を使用することを想定しています)はどのイベントで描画するべきか?
> マウス系イベントで元画像に描画するか、もしくは Paint イベントで前景描画するか、ですかね。
> 私なら、BackgroundImage に画像をセットしておいた上で、矩形は Paint で描画させると思います。
>
現在の処理としては、
PictureBox.Imageに対して、FromImageでGraphicsオブジェクトを取得し(gとする)
g.DrawImageで、描画を行い、Form.Refresh() をしています。
この流れで、
>Paintで描画させる
はForm.Paintでよいのでしょうか?

>
>>- PictureBox.Imageへ枠線を描画しましたが、拡大時(PictureBoxの外側へマウスを移動)には描画できませんでした(当たり前)
> その方法の場合、「描画結果も合わせた矩形サイズ」の Bitmap を
> Image に再割り当てしてから描画しないと駄目でしょうね。
> 元画像が 300x400 で、ドラッグ指定がその中の (50, -50)-(350, 500) だとしたら、
> 描画結果の画像は最低でも 350x550 のキャンパスが必要になる計算です。
>
元のページに従い
PictureBox.SizeMode = AutoSizeとしています。
Imageをリサイズしてしまうとそれに伴って表示画像もリサイズされてしまいますよね?それは避けたいと考えています。
また、「スクロールバーを簡単に出す」ことを目的として上記ページを参考にしたつくりにしたので
この構造も崩したくないとも考えています。
そもそも、両立は難しい。ということであればご指摘いただければ幸いです。

>
>>- Panelに描画したものを、PictureBoxの上へ表示(背景の透過率100%とかで)することはできないものでしょうか?
> Panel で無くとも良いのなら、Microsoft.Ink.InkOvelay をフォームにアタッチさせるとか。
> Region で矩形に切り取った別ウィンドウを重ねて表現するとか。
> GetDC(Intptr.Zero) に対して、InvertRect 等で描画するとか。

すみません。不勉強で理解できませんでした。
こちらについては、別でトライしてみたいと思います。

よろしくお願いいたします。
■No29976に返信(Ryoさんの記事)
> とりあえずは右端をドラッグして左右に動かす

手抜き実装だと、こういう手もありますけれどね。

private Form childForm;
private PictureBox pictureBox1;
public Form1()
{
    //InitializeComponent();
    //this.AutoScroll = true;

    childForm = new Form();
    childForm.FormBorderStyle = FormBorderStyle.SizableToolWindow;
    childForm.ControlBox = false;
    childForm.TopLevel = false;
    childForm.SetBounds(50, 50, 50, 50);

    pictureBox1 = new PictureBox();
    pictureBox1.BackColor = Color.Transparent;
    pictureBox1.BorderStyle = BorderStyle.Fixed3D;
    pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
    pictureBox1.Dock = DockStyle.Fill;
    pictureBox1.Image = this.Icon.ToBitmap();
    childForm.Controls.Add(pictureBox1);

    this.Controls.Add(childForm);
    childForm.Visible = true;
}

これに
http://dobon.net/vb/dotnet/form/moveform.html
を組み合わせると、移動やリサイズも可能です。
■No29977に追記(魔界の仮面弁士の記事)
> これに
> http://dobon.net/vb/dotnet/form/moveform.html
> を組み合わせると、移動やリサイズも可能です。

上記 URL にある WM_NCHITTEST 案を用いて、
ドラッグでリサイズ可能な PictureBox を作ってみました。

PictureBox 自体がリサイズされるので、SizeMode は
StretchImage あたりに設定しておいてください。


public partial class ResizablePictureBox : PictureBox
{
    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if (DesignMode) return;

        const int WM_NCHITTEST = 0x84;
        if (m.Msg != WM_NCHITTEST) return;

        Point pos = new Point(m.LParam.ToInt32());
        int bw = SystemInformation.FrameBorderSize.Width;
        int bh = SystemInformation.FrameBorderSize.Height;

        if (m.Result == (IntPtr)HitTest.Client)
        {
            m.Result = (IntPtr)HitTest.Caption;
        }

        if (RectangleToScreen(new Rectangle(0, 0, bw, bw)).Contains(pos))
            m.Result = (IntPtr)HitTest.Caption;
        else if (RectangleToScreen(new Rectangle(Width - bw, 0, bw, bh)).Contains(pos))
            m.Result = (IntPtr)HitTest.TopRight;
        else if (RectangleToScreen(new Rectangle(0, Height - bh, bw, bh)).Contains(pos))
            m.Result = (IntPtr)HitTest.BottomLeft;
        else if (RectangleToScreen(new Rectangle(Width - bw, Height - bh, bw, bh)).Contains(pos))
            m.Result = (IntPtr)HitTest.BottomRight;
        else if (RectangleToScreen(new Rectangle(0, 0, Width, bw)).Contains(pos))
            m.Result = (IntPtr)HitTest.Top;
        else if (RectangleToScreen(new Rectangle(0, bw, Width, bh)).Contains(pos))
            m.Result = (IntPtr)HitTest.Caption;
        else if (RectangleToScreen(new Rectangle(0, 0, bw, Height)).Contains(pos))
            m.Result = (IntPtr)HitTest.Left;
        else if (RectangleToScreen(new Rectangle(Width - bw, 0, bw, Height)).Contains(pos))
            m.Result = (IntPtr)HitTest.Right;
        else if (RectangleToScreen(new Rectangle(0, Height - bh, Width, bh)).Contains(pos))
            m.Result = (IntPtr)HitTest.Bottom;
        else
            m.Result = (IntPtr)HitTest.Caption;
    }

    enum HitTest
    {
        Left = 10,
        Right = 11,
        Top = 12,
        TopLeft = 13,
        TopRight = 14,
        Bottom = 15,
        BottomLeft = 16,
        BottomRight = 17,
        Border = 18,

        Error = -2,         // ビープ音ありのNoWhere
        Transparent = -1,   // 他のウィンドウに覆い隠されている部分
        NoWhere = 0,        // 背景またはウィンドウ枠
        Client = 1,         // クライアントエリア
        Caption = 2,        // タイトルバー
        SysMenu = 3,        // システムメニュー
        SizeBox = 4,        // サイズボックス
        Menu = 5,           // メニュー
        HScroll = 6,        // 水平スクロールバー
        VScroll = 7,        // 垂直スクロールバー
        MinButton = 8,      // 最小化ボタン
        MaxButton = 9,      // 最大化ボタン
        Close = 20,         // [閉じる]ボタン
        Help = 21,          // ヘルプボタン
    }
}
■No29976に返信(Ryoさんの記事)
> PictureBox.SizeMode = AutoSizeとしています。
> Imageをリサイズしてしまうとそれに伴って表示画像もリサイズされてしまいますよね?

AutoSize なのですよね? たとえ
  pictureBox1.Width *= 2;
  pictureBox1.Height *= 2;
などとしても、表示画像はリサイズされないと思いますが…。


リサイズ時の画像表示結果については、Image の使い方にもよりますし、
SizeMode の指定によっても変わります。

画面構成がイメージしきれていないので、認識のずれがあるかも知れませんが、
Image のサイズと、表示する画像を同じサイズにする必要は無いかも知れません。


// 以下、素の Panel に直接描画しているので、ちょっとちらつきます。
// 必要に応じて、PictureBox などを併用してみてください。

public partial class Form1 : Form
{
    Image sourceImage;  // 元画像
    Panel panel1;       // スクロールエリア

    bool dragging;
    Point mousePos1, mousePos2;
    public Form1()
    {
        InitializeComponent();

        // 表示させる画像
        using (BinaryWriter bw = new BinaryWriter(new MemoryStream()))
        {
            bw.Write(File.ReadAllBytes(@"C:\temp\sample.png"));
            bw.Flush();
            sourceImage = Image.FromStream(bw.BaseStream);
        }
        // this.DoubleBuffered = true;

        Padding = new Padding(8);
        panel1 = new Panel();
        panel1.BorderStyle = BorderStyle.Fixed3D;
        panel1.Dock = DockStyle.Fill;
        panel1.AutoScroll = true;
        Controls.Add(panel1);
        Control ctrl = new Control();
        ctrl.TabStop = false;
        // スクロールさせる領域の大きさ
        // (元画像のサイズにするか、固定値にするかはお好みで)
        ctrl.SetBounds(1000, 1000, 0, 0);
        panel1.Controls.Add(ctrl);

        #region ドラッグ
        panel1.MouseDown += delegate(object o, MouseEventArgs arg)
        {
            dragging = true;
            mousePos1 = mousePos2 = Point.Add(panel1.PointToClient(MousePosition), new Size(panel1.AutoScrollOffset));
            panel1.Invalidate();
        };

        panel1.MouseMove += delegate(object o, MouseEventArgs arg)
        {
            if (dragging)
            {
                mousePos2 = Point.Add(panel1.PointToClient(MousePosition), new Size(panel1.AutoScrollOffset));
                panel1.Invalidate();
            }
        };

        panel1.MouseUp += delegate(object o, MouseEventArgs arg)
        {
            mousePos2 = Point.Add(panel1.PointToClient(MousePosition), new Size(panel1.AutoScrollOffset));
            dragging = false;
            panel1.Invalidate();
        };
        #endregion

        #region 描画
        panel1.Paint += delegate(object o, PaintEventArgs arg)
        {
            Size sz = new Size(sourceImage.Width / 2, sourceImage.Height / 2);
            arg.Graphics.DrawImage(sourceImage, new Rectangle(panel1.AutoScrollPosition, sz));

            if (dragging)
            {
                Rectangle dragArea = new Rectangle(
                    Math.Min(mousePos1.X, mousePos2.X), 
                    Math.Min(mousePos1.Y, mousePos2.Y), 
                    Math.Abs(mousePos1.X - mousePos2.X), 
                    Math.Abs(mousePos1.Y - mousePos2.Y));

                if (dragArea.Width > 0 && dragArea.Height > 0)
                {
                    ControlPaint.DrawFocusRectangle(arg.Graphics, dragArea);
                }
            }
        };
        #endregion
    }
}
■No29975に返信(Ryoさんの記事)
>>両方に描画すればいいんじゃないかな?
> ・PictureBoxに描画されるものは、PictureBoxへ描画。
> ・その外側になってしまうものはPanelへ描画
> というような感じでしょうか?
> ご教示いただいた内容は、DrawLineで各線を描画するイメージでよいでしょうか?

それでも実現できるとは思いますが、それぞれの領域からはみ出す様に
描画するだけでも良いかも知れません。
領域外にはみ出た分は、どうせ表示されないと思いますし。

もしも、はみ出して描画すると問題があるような画面構成であれば、
Graphics に対してクリッピング領域を指定することもできます。
http://dobon.net/vb/dotnet/graphics/setclip.html
■No29976に返信(Ryoさんの記事)
>>>- Mouseイベントはどのコントロール(Panel?PictureBox?両方?)で拾うべきか?
>>PictureBox 全体に画像が表示されるのが、PictureBox の一部領域だけに表示されるのかにもよりますが、
>>ドラッグ開始点が PictureBox の上にあるなら PictureBox、
>>ドラッグ開始点が Panel の上にあるなら Panel、
>>ドラッグ開始点が Form の上にあるなら Form という感じではないかと。
> つまり、すべてで同様に拾う必要があるということですね。
> なお、Panel.Dock = Fillとしているので、Formで拾う必要は無いかと思いますが合っていますか?

マウスがコントロールの領域外に出て行った後も、MouseMove や MouseUp は、
MouseDown されたコントロールに通知されるようになっています。

ですから、もしも Panel を押下してドラッグの開始させることがなく、
常に PictureBox からドラッグが開始されるような場合には、
マウスイベントは PictureBox だけに書けばよいという事になりますし、
Panel からも PictureBox からもドラッグを開始される場合があるのなら、
Panel と PictureBox の両方にイベント処理が必要になるでしょう。


>>>- Panelに描画したものを、PictureBoxの上へ表示(背景の透過率100%とかで)することはできないものでしょうか?
DrawImage などを用いて、自分で描画することはできるかと思います。

コントロールの機能としては…コントロール自体を透過させることはできません。
BackColor = Transparent を指定することで、親コントロールの背景色などを、
そのコントロールの背景として自動描画させることができる程度ですね。
http://www.vb-user.net/junk/replySamples/2010.12.07.17.15/Transparent.png


>>GetDC(Intptr.Zero) に対して、InvertRect 等で描画するとか。

ごめんなさい、間違えました。InvertRect は今回関係無かったです。
(InvertRect API は枠線描画では無く、矩形範囲の色を反転描画させるものです)

上記は、GetDC(IntPtr.Zero) に対して描画することで、自ウィンドウのみならず
その外側(デスクトップ画面全体)に対して描画することができますので、
これで、PictureBox と Panel の両方にまたがった描画も可能だという事を
示したつもりでした。(それが今回の目的に合うかどうかまでは分かりませんが)
http://d.hatena.ne.jp/d-kami/20090705/1246826888
http://homepage1.nifty.com/yasunari/VB/VB2005/DrawToScreen.htm
いろいろご教示いただきありがとうございます。

以下の方針で対応し、狙いの動作ができました。
・PanelとPictureBoxで以下のイベントを拾い、同じ処理を行う
- MouseMove
- MouseDown
- MouseUp
・元の領域より広くなる動作の場合は、PictureBoxを広げて、PictureBox全体に枠線を描く
・元の領域より小さくなる動作の場合は、PictureBoxを広げず、マウス位置までで枠線を描く
・ MouseUp(動作完了時)は、PictureBoxを元のサイズへ戻す
■No30033に返信(ryoさんの記事)
上記に対して、作成したコードは以下の通りです。

#region VALUE
static int MICON_CHG_THRESH = 5; // マウスアイコンの形状を変えるスレッシュ
static int LINE_WIDTH = 1; // ガイド枠の幅
#endregion

#region member
bool mIsDrawGuide; // ガイド枠を描画するかどうか
int mMousePos;
int mResizeStart; // リサイズ開始時のサイズ
#endregion

public Form1()
{
InitializeComponent();
mIsDrawGuide = false;
}

#region private/protected
private void resizeMouseMove(MouseEventArgs e)
{
mMousePos = e.X;

// マウスアイコンの変更
if (Math.Abs(mMousePos - pictureBox1.Width) < MICON_CHG_THRESH)
{
this.Cursor = Cursors.SizeWE;
}
else
{
if (mIsDrawGuide)
{
if (mMousePos > this.mResizeStart)
{
// ガイド枠描画中=リサイズ処理を行う
this.pictureBox1.Width = mMousePos;
}
this.Refresh();
}
else
{
this.Cursor = Cursors.Default;
}
}
}

private void resizeMouseDown(MouseEventArgs e)
{
if (this.Cursor == Cursors.SizeWE)
{
this.mResizeStart = this.pictureBox1.Width;
// リサイズ可能
mIsDrawGuide = true;

// 再描画
this.Refresh();
}
}

private void resizeMouseUp(MouseEventArgs e)
{
this.mIsDrawGuide = false;
this.pictureBox1.Width = mResizeStart; // 元に戻す
resizeMouseMove(e); // アイコンの変更(MouseMoveと同じ条件)
this.Refresh();
}
#endregion

#region マウス操作
// PictureBox
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
resizeMouseMove(e);
}

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
resizeMouseDown(e);
}

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
resizeMouseUp(e);
}

// Panel
private void panel1_MouseMove(object sender, MouseEventArgs e)
{
resizeMouseMove(e);
}
private void panel1_MouseDown(object sender, MouseEventArgs e)
{
resizeMouseDown(e);
}
private void panel1_MouseUp(object sender, MouseEventArgs e)
{
resizeMouseUp(e);
}

#endregion

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (mIsDrawGuide)
{ // ガイド枠を描画するとき
Graphics g;

g = e.Graphics;
Pen pen = new Pen(Color.Black, (float)LINE_WIDTH);
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
Point pStart = new Point(0, 0);
Size sRect = this.pictureBox1.Size;
sRect.Height -= (LINE_WIDTH * 2);
if (this.mMousePos > mResizeStart)
{ // 拡大したPictureBox全体に描画
sRect.Width -= (LINE_WIDTH * 2);
}
else
{
sRect.Width = this.mMousePos - (LINE_WIDTH * 2);
}
g.DrawRectangle(pen, new Rectangle(pStart, sRect));

pen.Dispose();
}
}
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板