Friday, August 27, 2004

 

Some notes on DotNet Multimedia Programming

1.
Using Animated Cursors in .NET

from http://vbaccelerator.com/home/NET/Code/Libraries/Graphics/Animated_Cursors_in__NET/article.asp

The Cursor class provided with System.Windows.Forms doesn't support animated cursors - but this tiny class lets you use them interchangably with the existing .NET Framework cursors. Also provided with the code is a technique for drawing cursors directly onto forms and animating on a background thread.

Animated Cursors
The Cursor class in System.Windows.Forms is another one of those sealed .NET Framework classes which doesn't support all of the facilities of the underlying API (another obvious example of this is the ImageList class). Creating a class which allows you to use animated cursors is nice and easy though, and although it can't extend the existing Cursor class, it can provide an instance of a Cursor object containing an animated cursor that you can use interchangably with the existing class.

AniCursor.cs Implementation
The class provides the following methods and properties:

Constructors
Constructors are provided for instantiating a cursor either from a file, a Win32 resource (although you will have to use a custom build process if you want to include a Win32 resource since VS.NET does not support adding Win32 resources) and directly from another cursor handle.
Cursor
Returns the animated cursor as a System.Windows.Forms.Cursor which you can assign to the Cursor property of any control or form.
Handle
Returns the handle of the current animated cursor, if any, or IntPtr.Zero otherwise.
FrameCount
Returns the number of frames within the currently loaded cursor.
Frame
Gets/sets the current frame of the cursor to draw using the Draw methods. The Step method moves to the next frame and manages looping back to the beginning of the animation.
Draw
These methods allow you to draw the current frame of the cursor onto a System.Drawing.Graphics object, optionally specifying the size and whether the cursor should be stretched to fit the available space.
Building AniCursor
The AniCursor class is really very simple to build since the powerful LoadImage and DrawIconEx API calls provided with user32.dll on Window Systems do almost all of the work. LoadImage is a multi-purpose function that can generally successfully load any bitmap, icon or cursor, including animated cursors, regardless of whether it is on disk or part of a resource in an executable or DLL. DrawIconEx can be used to draw elements of any icon or cursor including the frames of the cursors. The only thing that can't be done as things stand is to instantiate an animated cursor from a .NET resource file without writing it to an intermediate temporary file on disk. This is because LoadImage unfortunately does not provide a stream or byte array based overload.

Drawing on a Background Thread
Having experienced first-hand the pain involved in trying to create a free-threaded component using VB6, the .NET way comes as a breath of fresh air. Despite Matt Currland's best efforts in his great book "Advanced VB6", I was never entirely successful in getting a stable free-threaded component running. In .NET its a lot easier, but you still need to take a great deal of care to make sure things work properly.

This sample uses a modified version of some great code from the February 2003 issue of MSDN Magazine (AsyncStart.cs) which makes it particularly easy to create a background threaded class which interacts with the UI. The best elements of this class are that it does most of the work in provided a nice model for cancelling a background threaded class from the UI and assists you avoiding some of the many thousands of ways you can inadvertently create a deadlock.

Only the cancellation functionality of the sample is actually used since once you start running the Animated cursor on a background thread it just loops forever, sleeping and drawing. Rather than having to call across onto the UI thread to perform the drawing (which would be blocked by the UI thread) the code takes advantage of System.Drawing to draw directly onto the window handle instead, like the way the Common Controls AVI player control works. I'd be interested to hear if anyone has more direct experience of the legality of this method from a threading perspective as the .NET documentation isn't particularly friendly on the subject of the thread safety or otherwise of the GDI+ wrapper.

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace vbAccelerator.Components.Win32
{

///
/// Allows animated cursors to be loaded and displayed
///

/// Note we cannot extend Cursor since it
/// is sealed, which is a shame.

public class AniCursor : IDisposable, ICloneable
{
#region Unmanaged Code
///
/// Draws an icon or cursor.
///

/// Be careful when using this function - I have successfully
/// blue-screened my system by settting an incorrect (negative)
/// iStepIfAniCur value. This may be because it is implemented
/// in the graphics driver.

[DllImport("user32")]
private extern static int DrawIconEx(
IntPtr hDC,
int xLeft,
int yTop,
IntPtr hIcon,
int cxWidth,
int cyWidth,
int istepIfAniCur,
IntPtr hbrFlickerFreeDraw,
int diFlags);

[DllImport("user32", CharSet = CharSet.Auto)]
private extern static IntPtr LoadImage(
IntPtr hInst,
string lpsz,
int uType,
int cx,
int cy,
int uFlags);

[DllImport("user32", CharSet = CharSet.Auto)]
private extern static IntPtr LoadImage(
IntPtr hInst,
int lpsz,
int uType,
int cx,
int cy,
int uFlags);

[DllImport("user32")]
private extern static IntPtr CopyImage(
IntPtr handle,
int uType,
int cxDesired,
int cyDesired,
int uFlags);

[DllImport("user32")]
private extern static int DestroyCursor(
IntPtr hCursor);

private const int IMAGE_BITMAP = 0x0;
private const int IMAGE_ICON = 0x1;
private const int IMAGE_CURSOR = 0x2;

private const int LR_DEFAULTCOLOR = 0x0000;
private const int LR_MONOCHROME = 0x0001;
private const int LR_COLOR = 0x0002;
private const int LR_COPYRETURNORG = 0x0004;
private const int LR_COPYDELETEORG = 0x0008;
private const int LR_LOADFROMFILE = 0x10;
private const int LR_LOADTRANSPARENT = 0x20;
private const int LR_DEFAULTSIZE = 0x0040;
private const int LR_LOADMAP3DCOLORS = 0x1000;
private const int LR_CREATEDIBSECTION = 0x2000;
private const int LR_COPYFROMRESOURCE = 0x4000;

private const int DI_MASK = 0x1;
private const int DI_IMAGE = 0x2;
private const int DI_NORMAL = 0x3;
private const int DI_COMPAT = 0x4;
private const int DI_DEFAULTSIZE = 0x8;
#endregion

#region Member Variables
private System.Windows.Forms.Cursor cursor = null;
private int frameCount = 0;
private int frame = 0;
private IntPtr hCur = IntPtr.Zero;
#endregion

///
/// Makes an independent copy of this cursor
///

/// A copy of this object
public object Clone()
{
AniCursor clone;
if (hCur != IntPtr.Zero)
{
IntPtr hCurClone = CopyImage(hCur, IMAGE_CURSOR, 0, 0, 0);
clone = new AniCursor(hCurClone);
}
else
{
clone = new AniCursor();
}
return clone;
}

///
/// Gets the handle of the cursor
///

public IntPtr Handle
{
get
{
return this.hCur;
}
}

///
/// Gets a Cursor containing the specified animated cursor
///

public System.Windows.Forms.Cursor Cursor
{
get
{
return this.cursor;
}
}
///
/// Returns the number of frames in this animated cursor
///

public int FrameCount
{
get
{
return this.frameCount;
}
}
///
/// Steps to the next frame in the animation
///

public void Step()
{
this.frame++;
if (this.frame >= this.frameCount)
{
this.frame = 0;
}
}
///
/// Gets/sets the current cursor frame
///

public int Frame
{
get
{
return this.frame;
}
set
{
if ((value < 0) || (value > this.frameCount))
{
throw new ArgumentException("Frame must be between 0 and
FrameCount-1", "Frame");
}
this.frame = value;
}
}
///
/// Draws the cursor at the specified position
///

/// Graphics object to draw on
/// X position to draw at
/// Y position to draw at
public void Draw(
System.Drawing.Graphics gfx,
int x,
int y
)
{
Draw(gfx, x, y, 0, 0, false);
}
///
/// Draws the cursor at the specified position
///

/// Graphics object to draw on
/// X Position to draw at
/// Y Position to draw at
/// Width
/// Height
public void Draw(
System.Drawing.Graphics gfx,
int x,
int y,
int width,
int height,
bool stretch
)
{
if (!stretch)
{
width = 0;
height = 0;
}
IntPtr hdc = gfx.GetHdc();
DrawIconEx(
hdc,
x, y,
this.hCur,
0, 0,
this.frame,
IntPtr.Zero,
DI_NORMAL);
gfx.ReleaseHdc(hdc);
}
///
/// Draws the cursor in the specified rectangle
///

/// Graphics object to draw on
/// Rectangle to draw at
public void Draw(
System.Drawing.Graphics gfx,
System.Drawing.Rectangle rect,
bool stretch
)
{
Draw(gfx, rect.X, rect.Y, rect.Width, rect.Height, stretch);
}
///
/// Loads an animated cursor from a file
///

/// Filename to load from
public void Load(
string fileName
)
{
clearCursor();
hCur = LoadImage(
Marshal.GetHINSTANCE(this.GetType().Module),
fileName,
IMAGE_CURSOR,
0, 0,
LR_LOADFROMFILE);
if (hCur != IntPtr.Zero)
{
evaluateFrames();
createCursor();
}
}

private void evaluateFrames()
{
// the only way to get the frames appears to
// be by trying to draw a frame... ugly
this.frameCount = 0;
this.frame = 0;
Bitmap bm = new Bitmap(128,128);
Graphics gfx = Graphics.FromImage(bm);
IntPtr hdc = gfx.GetHdc();
int success = 1;
while (success != 0)
{
success = DrawIconEx(hdc, 0, 0, hCur, 0, 0, this.frameCount,
IntPtr.Zero, DI_NORMAL);
if (success != 0)
{
this.frameCount++;
}
}
gfx.ReleaseHdc(hdc);
}

private void createCursor()
{
this.cursor = new Cursor(this.hCur);
}

private void clearCursor()
{
if (this.hCur != IntPtr.Zero)
{
DestroyCursor(this.hCur);
this.hCur = IntPtr.Zero;
}
if (this.cursor != null)
{
this.cursor.Dispose();
}
}

///
/// Constructs a blank instance of the AniCursor
/// class
///

public AniCursor()
{
}
///
/// Constructs a new instance of the class
/// and loads an animated cursor from the
/// specified file.
///

/// File containing animated cursor
public AniCursor(
string fileName
)
{
Load(fileName);
}
public AniCursor(
IntPtr hInstance,
int resourceId
)
{
LoadImage(hInstance, resourceId, IMAGE_CURSOR, 0, 0, 0);
}
public AniCursor(
IntPtr hInstance,
string resourceName
)
{
LoadImage(hInstance, resourceName, IMAGE_CURSOR, 0, 0, 0);
}
///
/// Constructs a new instance of the class from the
/// specified cursor handle.
///

///
public AniCursor(
IntPtr handle
)
{
this.hCur = handle;
evaluateFrames();
createCursor();
}
///
/// Clears up any resources associated with the animated cursor
///

public void Dispose()
{
clearCursor();
}
}
}

2.
DotNet中GIF动画的显示和截图

最近又有人提出这两个问题,不妨来说说。

VC下面,我提倡使用
Adding GIF-animation using GDI+
By norm.net
http://www.codeproject.com/vcpp/gdiplus/imageexgdi.asp
我在以往的程序中多次使用,效果不错。其中的线程控制中规中矩,值得学习。

最近油看到了C#的实现,居然完全是不同的打法

作者:Justin Rogers
出处:http://weblogs.asp.net/justin_rogers/archive/2004/01/19/60424.aspx

Looped GIF Animations in GDI+
Animated image display in GDI+ is fairly simple. They give you a special ImageAnimator class that internally stores a 50 ms timer to figure out when frames in an image should be updated, throws an event to control the update process in case you need to do something, and then gives you control to advance the frame. So let's take a basic animated gif and see how easy it would be to animate.

Image animatedImage = Image.FromFile("MyAnimation.gif");
if ( ImageAnimator.CanAnimate(animatedImage) ) {
ImageAnimator.Animate(animatedImage, new EventHandler(this.Image_FrameChanged));
}

public void Image_FrameChanged(object sender, EventArgs e) {
// Try to call the image overload if possible
ImageAnimator.UpdateFrames();
}

Now depending on how you are drawing the image (usually in a Paint handler for some sort of control) whenever UpdateFrames is called the next portion of the image will be made ready to display. This'll give you basic animation. One of the problems I've found when doing this is that all animations loop forever. ImageAnimator has no concept of a loop count, nor does it allow access to any properties that define how many loops have occured, what the current frame is, or how many frames are available in the animation. All of this has to be added by the programmer (total ballz if you ask me). So what can we do to help with this? Well, we have to define our own AnimationInfo structure to store the information needed to count loops. This is already being stored privately within the GDI+ code, they just don't give you access. So here goes, a class to hold loop counts, frame counts, get the total number of frames based on an Image, and a frame advancement method.

private class AnimationInfo {
private int currentLoop = 0;
private int currentFrame = 0;
private int totalFrames = 0;

public AnimationInfo(Image image) {
this.currentLoop = 0;
this.currentFrame = 0;
this.totalFrames = image.GetFrameCount(new FrameDimension(image.FrameDimensionsList[0]));
}

public AnimationInfo(int currentLoop, int currentFrame, int totalFrames) {
this.currentLoop = currentLoop;
this.currentFrame = currentFrame;
this.totalFrames = totalFrames;
}

public void UpdateFrames() {
currentFrame++;
if ( currentFrame >= totalFrames ) {
currentFrame = 0;
currentLoop++;
}
}

public int CurrentLoop { get { return this.currentLoop; } }
public int CurrentFrame { get { return this.currentFrame; } }
public int TotalFrames { get { return this.totalFrames; } }
}

Now we have the ability to control how many loops we do. But we still need to jump through some hoops to figure out how MANY loops to do. Thankfully the Image class has the ability to get extended image properties like loop count for animated gifs, it just isn't very accessible. The following code will investigate a file in order to determine if an animation should be infinite or finite based on the artists original intention.

public bool SetAnimationLoopsFromImage() {
if ( this.internalImage.RawFormat.Equals(ImageFormat.Gif) ) {
int loops = 0; // 0 will be infinite

PropertyItem propLoop = this.internalImage.GetPropertyItem(20737);
if ( propLoop == null ) {
loops = 1; // Some packages will remove the prop when loop should be only 1
} else {
loops = propLoop.Value[0];
loops += propLoop.Value[1] << 8;

if ( loops != 0 ) {
loops++; // Don't ask me, I guess it is part of the spec
}
}

this.animationMode = AnimationMode.ImageAnimation;
this.animationLoops = loops;
return true;
}

return false;
}

So tying all this together, we can now loop an animation based on data within a file and control how many times the animation loops before finally settling down. The remaining code looks as follows and would be part of the code used to derive a new animated picture box based on the System.Windows.Forms.Control class. You could use a PictureBox, but it does some work when you attach an image to initialize the ImageAnimator and you don't want the PictureBox code stepping on you.

private void Image_FrameChanged(object sender, EventArgs e) {
this.Invalidate();
}

protected override void OnPaint(PaintEventArgs pe) {
if ( animationState == AnimationState.Animating ) {
if ( animationLoops == 0 || this.animationInfo.CurrentLoop < animationLoops ) {
this.animationInfo.UpdateFrames();

Console.WriteLine(this.animationInfo.CurrentFrame);
Console.WriteLine(this.animationInfo.CurrentLoop);
ImageAnimator.UpdateFrames(this.internalImage);
} else {
Stop(true);
}
}

GraphicsUnit gu = GraphicsUnit.Pixel;
pe.Graphics.DrawImage(
this.internalImage,
this.internalImage.GetBounds(ref gu));

base.OnPaint(pe);
}

Hopefully this is somewhat enlightening for users out there that want basic animated buttons without having to resort to sprite animation and/or DirectX. Animated gifs already have the ability to be extremly compressed, so much so that only very small regions of the image might be updated during a frame change. Using this to your advantage can result in some very professional looking hover effects or status indicators. If I get the chance I'll more fully define a control that provides all of the functionality define in the article. I currently have one, but I'm still not satisfied with the feature set, the API interface, or the documentation to let it loose on the world.

参见ImageAnimator Class 说明
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemdrawingimageanimatorclasstopic.asp

我根本就没想到还有这么一个类存在,呵呵。

对了还有截图问题
作者:Justin Rogers
出处:http://weblogs.asp.net/justin_rogers/archive/2004/05/03/125228.aspx

Follow-up on Screen Capturing in Whidbey, I'm a fool who doesn't do all of his homework...
In my previous article If you could pick the reason why GDI should be .NET accessible, what would it be? I talked about the new GDI namespace. Now, while I was searching and searching for a way to do screen captures on the GDI classes, they instead added the screen capture to the actual Graphics class. I could go into why this is such a band-aid, but I won't. Instead I'll show you how to use it, then talk about how it is a band-aid ;-)

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;

public class ScreenCap {
private static void Main(string[] args) {
if ( args.Length < 1 ) {
return;
}

string fileName = args[0];
ScreenCapture(fileName);
}

private static void ScreenCapture(string fileName) {
Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
Graphics gfx = Graphics.FromImage(bmp);
gfx.CopyFromScreen(0, 0, 0, 0, bmp.Size);
gfx.Dispose();
bmp.Save(fileName, ImageFormat.Jpeg);
}
}

The above code makes a screen capture. It takes a while to load and run, which I don't find very amusing, but what the heck, I can't complain. You can extend this with my previous article to show how you might first copy the screen, modify it, and then write it back out for a screen saver. I'll do something along these lines soon, since I have a large amount of GDI+ filters written that would be cool to render animated over the desktop.

Now I want to talk about how much of a band-aid this is. Any copy operation from the screen is doing a BitBlt somewhere. A BitBlt can and should be allowed between ANY two surfaces, but they are limiting access to the copy operation to only screen surfaces, and not allowing blt's between offscreen surfaces. Rather than give a generic method for copying from any hDC or device context, which would have been really nice, especially for DirectX + GDI interfacing, they instead give you an operation to only copy the entire screen. I'll stop now. Enjoy your screen capture code and watch for my upcoming screen saver section.

正是方便呀。另外,Justin Rogers绝对是个代码疯子。

3.
PictureBox Load Image From Web

作者:孙展波
出处:http://blog.joycode.com/zhanbos/archive/2004/07/19/27765.asp

在周末休闲之.NET Quiz (9) 中有这样一道题目:

已知在此处可以看到Tech Ed 2003 China会场的照片一:http://www.microsoft.com/china/techED/images/a04.jpg

在一个WinForm应用程序中的用户界面包含一个Button控件btn1和PictureBox控件pictureBox1(pictureBox1的SizeMode为AutoSize)。当用户点击btn1时,其Event Handler就会读取那张照片并且在pictureBox1中显示出来。只用一行代码实现这个功能,请填空:

pictureBox1.______ = ______________________________________;

因为这里没有特别指出使用WinForm的版本号,我期待的答案是:
pictureBox1.Image = new Bitmap((new System.Net.WebClient()).OpenRead("http://www.microsoft.com/china/techED/images/a04.jpg "));

Ninputer写出了:
pictureBox1.Image = Image.FromStream(System.Net.WebRequest.Create("http://www.microsoft.com/china/techED/images/a04.jpg ").GetResponse().GetResponseStream());

在WinForm2.0下,一个新的property(属性)使得这一操作变得非常简单:
this.pictureBox1.ImageLocation = "http://www.microsoft.com/china/techED/images/a04.jpg ";

重粒子也在Quiz中提到了这一属性。在Winform Designer中对其解释是:Disk or web location to load image from.

(按:DotNet的花式就是多。不过反汇编看看源代码,基本函数还是那么几个。WinForm2.0的方法简单,出错有提示,首选)



<< Home

This page is powered by Blogger. Isn't yours?