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

C#で、画像の上に落書きをしたい

環境/言語:[Visual C# 2005 Express Edition, Win XP]
分類:[その他]

(他の掲示板に質問させてもらったのですが、アクセスできなくなってて・・・)

画像を読み込み、その上にペンで落書きをしたり、
消しゴムで落書きを消したりしたいのですが、
消しゴムで落書きを消した際、背景の画像まで消えてしまいます。

背景画像に影響を与えないように、落書きをしたり、消しゴムで
消したりする方法はあるのでしょうか?

どなたか、ご存知の方アドバイスお願いいたします。
C#を始めたばかりなもので、具体的な解決策を提示していただけると
助かります。
> (他の掲示板に質問させてもらったのですが、アクセスできなくなってて・・・)

復活しています。
http://bbs.wankuma.com/

> 背景画像に影響を与えないように、落書きをしたり、消しゴムで
> 消したりする方法はあるのでしょうか?

無いです。
「描き潰してしまったもの」を「描く前の状態に戻す」ことはできません。

なので、背景画像を保存しておき、「消しゴムでこすった所に背景画像を部分的に描画する」という方向で考えます。
渋木宏明(ひどり)さんレスありがとうございます。


>>背景画像に影響を与えないように、落書きをしたり、消しゴムで
>>消したりする方法はあるのでしょうか?
>
> 無いです。
> 「描き潰してしまったもの」を「描く前の状態に戻す」ことはできません。
>
> なので、背景画像を保存しておき、「消しゴムでこすった所に背景画像を部分的に描画する」という方向で考えます。

不透明な紙に背景画像を描いて、その上に透明(例:白は透明として扱う)な落書き用の紙を置いて対応できないものかと考えたのですが、そうはいかないんですか。

背景画像を部分描画する方向で、やり方を調べて見ます。
> 不透明な紙に背景画像を描いて、その上に透明(例:白は透明として扱う)な落書き用の紙を置いて対応できないものかと考えたのですが、そうはいかないんですか。

Bitmap クラスは透明色をサポートしているので出来ないことはないですが、かえって多くの手数が必要になります。(はず)
渋木宏明(ひどり)さん、早速レスありがとうございます。

>>不透明な紙に背景画像を描いて、その上に透明(例:白は透明として扱う)な落書き用の紙を置いて対応できないものかと考えたのですが、そうはいかないんですか。
>
> Bitmap クラスは透明色をサポートしているので出来ないことはないですが、かえって多くの手数が必要になります。(はず)

pictureBoxと下記の組み合わせで上手いこといかないものだろうかなんて、甘い期待を寄せていたりもしたのですが・・・
http://msdn.microsoft.com/library/ja/default.asp?url=/library/ja/vbcon/html/vbtskgivingyourcontroltransparentbackground.asp
> pictureBoxと下記の組み合わせで上手いこといかないものだろうかなんて、甘い期待を寄せていたりもしたのですが・・・

試してみては?
渋木宏明(ひどり)さん、レスありがとうございます。

>>pictureBoxと下記の組み合わせで上手いこといかないものだろうかなんて、甘い期待を寄せていたりもしたのですが・・・
>
> 試してみては?
試している最中なんですが、C#初心者には壁が高くて簡単に試せず、四苦八苦しています。
2007/01/17(Wed) 19:54:00 編集(投稿者)

> 試している最中なんですが、C#初心者には壁が高くて簡単に試せず、四苦八苦しています。

具体的に、どういったところが問題になっていますか?
渋木宏明(ひどり)さん、度々レスありがとうございます。

>>試している最中なんですが、C#初心者には壁が高くて簡単に試せず、四苦八苦しています。
>
> 具体的に、どういったところが問題になっていますか?

コントロールを透明にして、下の画像を表示することはできるようになりました。
その後色々調べてみたのですが、フォームは指定色を透明にできるのに対し、コントロールは指定色を透明にするということはできないように思えます。
コントロールの指定色を透明にすることは可能なのでしょうか?


一応、この方法には問題があるかもと思いもう一度実現方法を考え直してみました。

1)フォーム1を作成し、フォーム1のBackgroundImageに任意の画像をしていする。
2)フォーム2をフォーム1と重なるように作成し、白が透明となるようなプロパティを設定する。(最初フォーム2は白にしておき、フォーム1が透けるようにさせる)
3)フォーム2に、落書きをしたり、消しゴムで消したり(任意の部分を白で塗りつぶす)すれば、フォーム1に表示してある任意の画像に影響なく落書きをしたり消したりできるはず・・・
4)フォーム2終了時には、フォーム1も終了させるようにする。

と、まぁこんな感じなのですが、果たしてどうやってフォームを複数制御するのやら? それともこんなことはできないのか?

などと、思っている次第です。
#頭の整理がついていない状態(方針が決まっていない)ですので、
#内容がわかりにくいかもしれません。スミマセン。
> コントロールの指定色を透明にすることは可能なのでしょうか?

直接的には厳しいです。
Region を使ってくり貫く位しか、見栄えのする方法はないと思います。

ちなみに、WPF を使えばほぼこの方針でコードが書けるはずです。

> と、まぁこんな感じなのですが、果たしてどうやってフォームを複数制御するのやら? それともこんなことはできないのか?

その案も厳しいです。
フォームの上にフォームを重ねて表示することはできますが、フォーム移動等の操作を2枚のフォームに対して完全に同時に行う方法がありません。
最終的にやりたいことは、
http://www.nue.ci.i.u-tokyo.ac.jp/%7Emasa-u/mmtk/index-ja.html
にある、「コードサンプル: マルチマウスペイントソフト」を改良して
背景に任意の画像を表示させ、その上にみんなで落書き。
消しゴムで消したときには、落書きだけが消えるというものを
作りたいと思っています。

なので、渋木宏明(ひどり)さんが初めにアドバイスしてくださった
消したい部分に、もう一度背景画像を書いてしまうという方法でも
いいのですが、これまたどうして実現していいものやら・・・
#ソースを見ても、なんとなくやりたい事の気持ちは伝わってくるの
#ですが、さて手を加えるとなると具体的にどこをどうすればいいのやら
#といった状況です。
> なので、渋木宏明(ひどり)さんが初めにアドバイスしてくださった
> 消したい部分に、もう一度背景画像を書いてしまうという方法でも
> いいのですが、これまたどうして実現していいものやら・・・

発言を見る限り、自力で実装する力はありそうですけどね。
「段取りが分からない」ということなら、以下を参考にしてみてください。

Bitmap backgroundBitmap = new Bitmap(適宜);
pictureBox.Image = backgroundBitmap;

で、PictureBox に背景画が表示されるのは分かりますよね?

これをひとひねりして、

Bitmap backgroundBitmap = new Bitmap(適宜);
Bitmap offscreenBitmap = new Bitmap(backgroundBitmap);
pictureBox.Image = offscreenBitmap;

とすると、backgroundBitmap の画素が offscreenBitmap にコピーされ、それが PictureBox に表示されます。

そして、「落書き」は offscreenBitmap に描画するものとします。具体的には

using(Graphics g = Graphics.FromImage(offscreenBitmap))
{
// g に対する描画
}

pictureBox.Refresh(); // PictureBox に再描画を要請

という流れで「落書き」します。

「消しゴム」もほとんど同じです。

「消しゴム」の大きさや「消しゴム」を適用する座標位置は分かっている(少なくとも何がしかの計算をすれば求まる)はずで、仮に

int x = 100; // 消しゴムを適用する x 座標
int y = 100; // 消しゴムを適用する x 座標
int cx = 15; // 消しゴムの幅
int cy = 15; // 消しゴムの高さ

とします。

この時

using(Graphics g = Graphics.FromImage(offscreenBitmap))
{
Rectangle rect = new Rectangle(x, y, cx, cy);
g.DrawImage(backgroundBitmap, rect, rect, GraphicsUnit.Pixel);
}

pictureBox.Refresh(); // PictureBox に再描画を要請

で、「1回消しゴムを当てた」のと同じ効果が得られるはずです。

なお、「落書き」と「消しゴム」で、それぞれ pictureBox.Refresh() に代わるもっと効率の良い方法もありますが、それは自分で考えてみてください。
渋木宏明(ひどり)さん、ご丁寧なアドバイスありがとうございます。
本日は、他のシステムの対応があるので後日試してご報告します。
渋木宏明(ひどり)さんの、サンプルを元にpictureBoxを利用するよう改造するのは、今の私では時間的に無理と感じましたので、http://www.nue.ci.i.u-tokyo.ac.jp/%7Emasa-u/mmtk/index-ja.htmlの大元のサンプルで、消しゴムで消したときにオフスクリーンを白で塗りつぶしている部分を、オフスクリーンに背景画像から該当領域をコピーする方針で行うことにしました。

ほぼできたのですが、消しゴムで消したときの背景画像の位置がややズレてしまいます。ズレ量は一定ではありません。
縦方向は、画面上部はズレが少ないのですが、画面下部に行くに従ってズレが大きくなります。
横方向は、微妙ですが画面左はズレが少ないのですが、画面右に行くに従ってズレが大きくなります。

色々試していますが、もう自力ではどうしていいかわかりません。
(タイムリミットも迫っています(T_T))
ソースを記載しますので、どなたか原因お分かりの方ご指摘ください。
(字数制限があるので若干割愛します)
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

// 画像キャプチャ
using System.Drawing.Imaging;
using System.Diagnostics;


using MMTk; // MMTkを使います

namespace mm_example_cs
{
public class Form1 : System.Windows.Forms.Form
{
private System.ComponentModel.Container components = null;
private Image m_bmpHaikei = null; // 背景画像 sawa
private Image m_bmpBack = null; // 背景画像 sawa

public Form1()
{
大元に同じ
}

protected override void Dispose( bool disposing )
{
大元に同じ
}

#region Windows フォーム デザイナで生成されたコード
private void InitializeComponent()
{
大元に同じ
}
#endregion

[STAThread]
static void Main()
{
{
// Bitmapオブジェクトにスクリーン・キャプチャ
Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width,
Screen.PrimaryScreen.Bounds.Height,
PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0,
Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
}

// ビットマップ画像として保存して表示
string filePath = @".\screen.bmp";
bmp.Save(filePath, ImageFormat.Bmp);
}

Application.Run(new Form1());
}

private System.Windows.Forms.Button clearBtn;

private bool m_mminited = false; // マルチマウス環境を初期化したかどうか
private int m_ncurs = 0; // カーソルの個数
private Bitmap m_bitmap; // オフスクリーンキャンバス
private Bitmap m_bitmapBack; // オフスクリーンキャンバス
private UserRec[] m_users = null; // カーソルごとの状態を持つレコード
private Pen[] m_pen = { new Pen(Color.Cyan, 3), new Pen(Color.Red, 3), new Pen(Color.Blue, 3), new Pen(Color.LightSkyBlue, 3) };

public Bitmap TheBitmap { get { return (m_bitmap); } }

public Bitmap TheBitmapBack { get { return (m_bitmapBack); } }

private void Form1_Load(object sender, System.EventArgs e)
{
// フォームを最大化する
this.WindowState = FormWindowState.Maximized;
//
ClearBitmap();
BltBitmap();

{
// 背景画像読み込み sawa
if (m_bmpBack == null)
{
m_bmpBack = Image.FromFile(@".\screen.bmp");
}
Bitmap newbm = new Bitmap(this.Width, this.Height);
Graphics g = Graphics.FromImage(newbm);
g.DrawImage(m_bmpBack, 0, 0, this.Width, this.Height); // sawa
if (m_bitmapBack != null)
{
g.DrawImage(m_bitmapBack, 0, 0, this.Width, this.Height);
}
m_bitmapBack = newbm;
}



if (MMManager.Start() == 0) // MMManagerの初期化
{
m_mminited = true;
MMManager.Register(this.Handle, true, false); // このウインドウを登録
MMManager.SetAutoConstraint(true); // 自動カーソル移動範囲制限あり

MMManager.AddChildWindow(clearBtn.Handle, -1); // Clearボタンを登録

m_ncurs = MMManager.GetCurrentNumCursors(); // カーソル個数を取得
m_users = new UserRec[m_ncurs];
for (int i = 0; i < m_ncurs; i++)
{
int iPenColor = i % 4;
m_users[i] = new UserRec(this, MMManager.GetCursor(i), m_pen[iPenColor]);

// イベントコールバック関数の設定
// この例ではカーソルごとに違ったインスタンスへコールバックするようにします
MMManager.SetEventCallback(new MMEventCallbackProcPtr(m_users[i].MMEVT_ALL), i, -1);
}
}
else
MessageBox.Show(this, "MManager.Start() failed.", "ERROR", MessageBoxButtons.OK);

ReshapeBitmap();
}

protected override void WndProc(ref Message m)
{
大元に同じ
}

protected override void OnSizeChanged(EventArgs e)
{
大元に同じ
}

protected override void OnPaint(PaintEventArgs e)
{
大元に同じ
}

private void clearBtn_Click(object sender, System.EventArgs e)
{
大元に同じ
}
private void ReshapeBitmap()
{
Bitmap newbm;
Graphics g;

// 背景画像読み込み sawa
if (m_bmpHaikei == null)
{
m_bmpHaikei = Image.FromFile(@".\screen.bmp");
}


newbm = new Bitmap(this.Width, this.Height);
g = Graphics.FromImage(newbm);
g.DrawImage(m_bmpHaikei, 0, 0, this.Width, this.Height); // sawa

if (m_bitmap != null)
g.DrawImageUnscaled(m_bitmap, 0, 0);
m_bitmap = newbm;
}

private void ClearBitmap()
{
Graphics g = Graphics.FromImage(m_bitmap);
g.DrawImage(m_bmpHaikei, 0, 0, this.Width, this.Height); // sawa
}

public void BltBitmap()
{
大元に同じ
}
public void BltBitmap(Rectangle r)
{
大元に同じ
}


public class UserRec
{
public UserRec(Form1 owner, CursorPlotter cursor, Pen pen)
{
m_owner = owner;
m_mode = InputMode.Pencil;
m_cursor = cursor;
m_pen = pen;
UpdateCursorAppearance();
}
private Form1 m_owner;
private CursorPlotter m_cursor;
private Pen m_pen;

private MMTk.POINT m_lastPt;
private long m_lastTime;

private enum InputMode
{
Pencil,
Eraser
};
private InputMode m_mode;

public void MMEVT_ALL(IntPtr owner, int curID, int evtTyp, int evtArg, ref MMMouseStat ms)
{
Graphics g = Graphics.FromImage(m_owner.TheBitmap);
Graphics gBack = Graphics.FromImage(m_owner.TheBitmapBack);
Rectangle r = new Rectangle(32767, 32767, -65535, -65535);
MMTk.POINT drawPt;

drawPt.x = drawPt.y = -32750;

switch ((MMTk.MMEvt)evtTyp)
{
大元に同じ
}

// 必要であれば描画処理をします
if ((drawPt.x >= -10000) || (drawPt.y >= -10000))
{
if (m_lastPt.x < r.X) { r.Width += r.X - m_lastPt.x; r.X = m_lastPt.x; }
if (m_lastPt.x > r.Right) r.Width = m_lastPt.x - r.X;
if (m_lastPt.y < r.Y) { r.Height += r.Y - m_lastPt.y; r.Y = m_lastPt.y; }
if (m_lastPt.y > r.Bottom) r.Height = m_lastPt.y - r.Y;

if (drawPt.x < r.X) { r.Width += r.X - drawPt.x; r.X = drawPt.x; }
if (drawPt.x > r.Right) r.Width = drawPt.x - r.X;
if (drawPt.y < r.Y) { r.Height += r.Y - drawPt.y; r.Y = drawPt.y; }
if (drawPt.y > r.Bottom) r.Height = drawPt.y - r.Y;

switch (m_mode)
{
case InputMode.Pencil: // えんぴつモード - 線を描く
g.DrawLine(m_pen/*Pens.Black*/, m_lastPt.x, m_lastPt.y, drawPt.x, drawPt.y);
break;
case InputMode.Eraser: // 消しゴムモード - 四角形の軌跡で消去
if (r.Width > r.Height)
{
for (int i = 0; i < r.Width + 1; i++)
{
/*org g.FillRectangle(Brushes.White,
m_lastPt.x + i * ((drawPt.x - m_lastPt.x > 0) ? 1 : -1),
m_lastPt.y + i * (drawPt.y - m_lastPt.y) / r.Width,
24, 24);
*/
Rectangle rect = new Rectangle(m_lastPt.x + i * ((drawPt.x - m_lastPt.x > 0) ? 1 : -1) /*-2*/,
m_lastPt.y + i * (drawPt.y - m_lastPt.y) / r.Width /*+12*/, 24, 24);
Rectangle Mrect = new Rectangle(m_lastPt.x + i * ((drawPt.x - m_lastPt.x > 0) ? 1 : -1),
m_lastPt.y + i * (drawPt.y - m_lastPt.y) / r.Width, 24, 24);
g.DrawImage(m_owner.m_bmpBack, Mrect, rect, GraphicsUnit.Pixel);
}
}
else if (r.Height > 0)
{
for (int i = 0; i < r.Height + 1; i++)
{
/*org g.FillRectangle(Brushes.White,
m_lastPt.x + i * (drawPt.x - m_lastPt.x) / r.Height,
m_lastPt.y + i * ((drawPt.y - m_lastPt.y > 0) ? 1 : -1),
24, 24);
*/
Rectangle rect = new Rectangle(m_lastPt.x + i * (drawPt.x - m_lastPt.x) / r.Height /*-2*/,
m_lastPt.y + i * ((drawPt.y - m_lastPt.y > 0) ? 1 : -1) /*+12*/, 24, 24);
Rectangle Mrect = new Rectangle(m_lastPt.x + i * (drawPt.x - m_lastPt.x) / r.Height,
m_lastPt.y + i * ((drawPt.y - m_lastPt.y > 0) ? 1 : -1), 24, 24);
g.DrawImage(m_owner.m_bmpBack, Mrect, rect, GraphicsUnit.Pixel);
}
}
else
{
//org g.FillRectangle(Brushes.White, drawPt.x, drawPt.y, 24, 24);
Rectangle rect = new Rectangle(drawPt.x/*-2*/, drawPt.y/*+12*/, 24, 24);
Rectangle Mrect = new Rectangle(drawPt.x, drawPt.y, 24, 24);
g.DrawImage(m_owner.m_bmpBack, Mrect, rect, GraphicsUnit.Pixel);

}
r.Width += 24;
r.Height += 24;
break;
}

// 描画された領域より少し広い領域の画像を画面に転送します。
r.Inflate(10, 10);
m_owner.BltBitmap(r);

m_lastPt = drawPt;
m_lastTime = System.DateTime.Now.Ticks;
}
}

protected void UpdateCursorAppearance()
{
大元に同じ
}
};
}
}
キャプチャ画像をもうひとつ作り、消しゴムのところで元画像を切り取る時のpointをPointToScreenすることで、解決しました。
解決済み!

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