这里延续讲Led控件的第三种,即高分辨率的Led显示屏,它是由很多密集的发光二极管组成的阵列,和显示器的像素显示的原理类似。(显示器的像素是一种RGB蜂窝状密集排列)。
类似的例子,例如vs.net里面的图片,光标设计器,可以看到它可以把图片像素放大成密集网格显示。效果如下图所示:
这个控件的实现原理是很直观的,简单描述一下绘制的方法。即首先我们需要准备一个真实的图片作为复制源,称为源图(src bitmap),我们把它复制到led显示屏图片(dest bitmap)中,每个像素被我们放大成一个网格(cell),大小为(cellsize * cellsize),这些网格的间距是celldistance。一个cell对应的就是一个pixel。在cell的间隙之间,可能是背景色,或者是Grid线。
复制源图的每个像素到一个cell中,我们可以使用bitmap的getpixel,setpixel方法,但是这样做显然效率会非常低下。这种针对精确到像素的操作应该使用指针直接操作内存,首先我们需要获取在内存中的位图数据(BitmapData对象),并将它锁定,亦即告诉操作系统在现在不要移动这块内存。在处理前我们必须理解,位图数据在内存中是一块地址连续的存储区域。
解释一下BitmapData对象提供的两个最重要属性:
1.IntPtr Scan0:这是一个指针,指向数据的第一个像素的第一个通道,也就是说从这向后数,都是像素内容。通道的顺序是BGR,BGR,BGR,...,你必须记住通道的顺序,不要搞错。
2.int Stride:它称为扫描行宽度,这是一个非常关键的概念,它告诉你图片中的每一行在这块内存中占据的字节长度。请注意,基于bitmap的存储方式,这个数字一定是4的倍数,也就是说,一个扫描行需要凑成4字节的整数倍,因此可能在末尾具有一些补0的冗余字节。
因此扫描行宽度可以描述为下面的等式:
stride(字节)=Bitmap.Width(像素)*bpp/8+行尾补0字节数
(其中bpp为色深度,bits per pixel, 位/像素)
对RGB图像,具有三个通道,每个通道占据1个字节,可理解为:
stride=Bitmap.Width*3+行尾补0字节数
则行尾补0字节数=4-(Bitmap.Width*3)%4;
因此了解了以上概念,我们就可以用下面的代码操作像素了,对于一个位于(i,j)位置的像素,它的RGB可以按照如下的下标访问:
在处理完成以后,必须将bitmapData解锁内存。
由于以上操作需要使用unsafe代码,所以必须在项目属性中设置允许执行不安全代码。否则会编译出错。
现在,可以看一下绘制LedScreen Bitmap的函数:
在上面的函数中,暂时忽略了DrawGrid这个参数。并且我们给出了srcX,srcY,这是源图复制的起始点。这两个参数主要是基于实现ledscreen滚动字幕效果的考虑。例如不断递增srcX,并更新位图,可以实现水平方向的字幕滚动。
下面为demo中的代码,临时创建了一个图片,并把生成的led screen图片显示在一个picurebox中:
对于vs.net中的图片编辑器来说,它相当于celldistance=1的情况,并且在cell的间隔之间绘制了Grid,我发现这些grid线是由两种颜色交替而组成的,因此这可以保证Grid线不会被任何颜色遮盖住。
下面是源代码的下载链接。我原来以为这个代码直接复制就可以用了,但是考虑到一些朋友还是向我提出一些问题。我把原来项目的代码上传并发在这里。这个里面不仅有这个led“走马灯”效果,也包含我一些分形图形有关的代码。
(源代码下载):
https://files.cnblogs.com/hoodlum1980/FractalMaker.rar
类似的例子,例如vs.net里面的图片,光标设计器,可以看到它可以把图片像素放大成密集网格显示。效果如下图所示:
这个控件的实现原理是很直观的,简单描述一下绘制的方法。即首先我们需要准备一个真实的图片作为复制源,称为源图(src bitmap),我们把它复制到led显示屏图片(dest bitmap)中,每个像素被我们放大成一个网格(cell),大小为(cellsize * cellsize),这些网格的间距是celldistance。一个cell对应的就是一个pixel。在cell的间隙之间,可能是背景色,或者是Grid线。
复制源图的每个像素到一个cell中,我们可以使用bitmap的getpixel,setpixel方法,但是这样做显然效率会非常低下。这种针对精确到像素的操作应该使用指针直接操作内存,首先我们需要获取在内存中的位图数据(BitmapData对象),并将它锁定,亦即告诉操作系统在现在不要移动这块内存。在处理前我们必须理解,位图数据在内存中是一块地址连续的存储区域。
解释一下BitmapData对象提供的两个最重要属性:
1.IntPtr Scan0:这是一个指针,指向数据的第一个像素的第一个通道,也就是说从这向后数,都是像素内容。通道的顺序是BGR,BGR,BGR,...,你必须记住通道的顺序,不要搞错。
2.int Stride:它称为扫描行宽度,这是一个非常关键的概念,它告诉你图片中的每一行在这块内存中占据的字节长度。请注意,基于bitmap的存储方式,这个数字一定是4的倍数,也就是说,一个扫描行需要凑成4字节的整数倍,因此可能在末尾具有一些补0的冗余字节。
因此扫描行宽度可以描述为下面的等式:
stride(字节)=Bitmap.Width(像素)*bpp/8+行尾补0字节数
(其中bpp为色深度,bits per pixel, 位/像素)
对RGB图像,具有三个通道,每个通道占据1个字节,可理解为:
stride=Bitmap.Width*3+行尾补0字节数
则行尾补0字节数=4-(Bitmap.Width*3)%4;
因此了解了以上概念,我们就可以用下面的代码操作像素了,对于一个位于(i,j)位置的像素,它的RGB可以按照如下的下标访问:
//像素(i,j)
byte* p=(byte*)(void*)bmData.Scan0;
p[ stride*j + i*3 ]=(byte);//Blue channel
p[ stride*j + i*3 +1 ]=(byte);//Green channel
p[ stride*j + i*3 +2 ]=(byte);//Red channel
byte* p=(byte*)(void*)bmData.Scan0;
p[ stride*j + i*3 ]=(byte);//Blue channel
p[ stride*j + i*3 +1 ]=(byte);//Green channel
p[ stride*j + i*3 +2 ]=(byte);//Red channel
在处理完成以后,必须将bitmapData解锁内存。
由于以上操作需要使用unsafe代码,所以必须在项目属性中设置允许执行不安全代码。否则会编译出错。
现在,可以看一下绘制LedScreen Bitmap的函数:
1/// <summary>
2/// 创建一个Led显示屏位图
3/// </summary>
4/// <param name="srcBitmap">贴图的源图</param>
5/// <param name="nWidth">显示屏宽度</param>
6/// <param name="nHeight">显示屏高度</param>
7/// <param name="drawGrid">是否绘制网格</param>
8/// <param name="srcX">源图起始坐标x</param>
9/// <param name="srcY">源图起始坐标y</param>
10/// <param name="cellsize">像素格大小</param>
11/// <param name="celldistance">像素格间距</param>
12/// <returns></returns>
13public static Bitmap CreateLedBitmap(Bitmap srcBitmap,int nWidth,int nHeight,bool drawGrid,int srcX,int srcY,int cellsize,int celldistance)
14{
15 Bitmap bm=new Bitmap(nWidth,nHeight);
16 //这是bm全是黑色的。(都被设置为0)
17 //获得图片的内存
18 BitmapData bmData=bm.LockBits(
19 new Rectangle(0,0,bm.Width,bm.Height),
20 ImageLockMode.ReadWrite,
21 PixelFormat.Format24bppRgb);
22 //扫描行宽度
23 int stride=bmData.Stride;
24
25 //获取源图的bmdata
26 BitmapData srcData=srcBitmap.LockBits(
27 new Rectangle(0,0,srcBitmap.Width,srcBitmap.Height),
28 ImageLockMode.ReadOnly,
29 PixelFormat.Format24bppRgb
30 );
31
32 int strideSrc=srcData.Stride; //源图的行长度
33 //操作内存的不安全代码段
34 unsafe
35 {
36 byte* p=(byte*)(void*)bmData.Scan0;
37 byte* pSrc=(byte*)(void*)srcData.Scan0; //源图
38
39 for(int j=srcY;j<srcBitmap.Height;j++)
40 {
41 for(int i=srcX;i<srcBitmap.Width;i++)
42 {
43 int nmin=(cellsize+celldistance)*(j-srcY);
44 int nmax=nmin+cellsize;
45 int mmin=(cellsize+celldistance)*(i-srcX);
46 int mmax=mmin+cellsize;
47 //源图i,j位置的像素复制给一个目标图的一个cell块!
48 for(int n=nmin;(n<nmax && n<bm.Height);n++)
49 {
50 for(int m=mmin;(m<mmax && m<bm.Width);m++)
51 {
52 p[ stride*n + m*3 ]=pSrc[ strideSrc*j + i*3]; //B
53 p[ stride*n + m*3 +1 ]=pSrc[ strideSrc*j + i*3+1]; //G
54 p[ stride*n + m*3 +2 ]=pSrc[ strideSrc*j + i*3+2]; //R
55 }
56 }
57 }
58 }// </for i>
59 }// </unsafe>
60 srcBitmap.UnlockBits(srcData);
61 bm.UnlockBits(bmData);
62 return bm;
63}
2/// 创建一个Led显示屏位图
3/// </summary>
4/// <param name="srcBitmap">贴图的源图</param>
5/// <param name="nWidth">显示屏宽度</param>
6/// <param name="nHeight">显示屏高度</param>
7/// <param name="drawGrid">是否绘制网格</param>
8/// <param name="srcX">源图起始坐标x</param>
9/// <param name="srcY">源图起始坐标y</param>
10/// <param name="cellsize">像素格大小</param>
11/// <param name="celldistance">像素格间距</param>
12/// <returns></returns>
13public static Bitmap CreateLedBitmap(Bitmap srcBitmap,int nWidth,int nHeight,bool drawGrid,int srcX,int srcY,int cellsize,int celldistance)
14{
15 Bitmap bm=new Bitmap(nWidth,nHeight);
16 //这是bm全是黑色的。(都被设置为0)
17 //获得图片的内存
18 BitmapData bmData=bm.LockBits(
19 new Rectangle(0,0,bm.Width,bm.Height),
20 ImageLockMode.ReadWrite,
21 PixelFormat.Format24bppRgb);
22 //扫描行宽度
23 int stride=bmData.Stride;
24
25 //获取源图的bmdata
26 BitmapData srcData=srcBitmap.LockBits(
27 new Rectangle(0,0,srcBitmap.Width,srcBitmap.Height),
28 ImageLockMode.ReadOnly,
29 PixelFormat.Format24bppRgb
30 );
31
32 int strideSrc=srcData.Stride; //源图的行长度
33 //操作内存的不安全代码段
34 unsafe
35 {
36 byte* p=(byte*)(void*)bmData.Scan0;
37 byte* pSrc=(byte*)(void*)srcData.Scan0; //源图
38
39 for(int j=srcY;j<srcBitmap.Height;j++)
40 {
41 for(int i=srcX;i<srcBitmap.Width;i++)
42 {
43 int nmin=(cellsize+celldistance)*(j-srcY);
44 int nmax=nmin+cellsize;
45 int mmin=(cellsize+celldistance)*(i-srcX);
46 int mmax=mmin+cellsize;
47 //源图i,j位置的像素复制给一个目标图的一个cell块!
48 for(int n=nmin;(n<nmax && n<bm.Height);n++)
49 {
50 for(int m=mmin;(m<mmax && m<bm.Width);m++)
51 {
52 p[ stride*n + m*3 ]=pSrc[ strideSrc*j + i*3]; //B
53 p[ stride*n + m*3 +1 ]=pSrc[ strideSrc*j + i*3+1]; //G
54 p[ stride*n + m*3 +2 ]=pSrc[ strideSrc*j + i*3+2]; //R
55 }
56 }
57 }
58 }// </for i>
59 }// </unsafe>
60 srcBitmap.UnlockBits(srcData);
61 bm.UnlockBits(bmData);
62 return bm;
63}
在上面的函数中,暂时忽略了DrawGrid这个参数。并且我们给出了srcX,srcY,这是源图复制的起始点。这两个参数主要是基于实现ledscreen滚动字幕效果的考虑。例如不断递增srcX,并更新位图,可以实现水平方向的字幕滚动。
下面为demo中的代码,临时创建了一个图片,并把生成的led screen图片显示在一个picurebox中:
1Bitmap bm=new Bitmap(150,24);
2Graphics g=Graphics.FromImage(bm);
3g.FillRectangle(Brushes.DarkGreen,0,0,bm.Width,bm.Height);
4SolidBrush brush=new SolidBrush(Color.FromArgb(20,255,20));
5g.DrawString("hello cnblogs!",new Font("Arial",9f),brush,1,1);
6g.Dispose();
7Bitmap bm2=FigFactory.CreateLedBitmap(bm,750,120,false,0,0,5,3);
8this.pictureBox1.Image=bm2;
2Graphics g=Graphics.FromImage(bm);
3g.FillRectangle(Brushes.DarkGreen,0,0,bm.Width,bm.Height);
4SolidBrush brush=new SolidBrush(Color.FromArgb(20,255,20));
5g.DrawString("hello cnblogs!",new Font("Arial",9f),brush,1,1);
6g.Dispose();
7Bitmap bm2=FigFactory.CreateLedBitmap(bm,750,120,false,0,0,5,3);
8this.pictureBox1.Image=bm2;
对于vs.net中的图片编辑器来说,它相当于celldistance=1的情况,并且在cell的间隔之间绘制了Grid,我发现这些grid线是由两种颜色交替而组成的,因此这可以保证Grid线不会被任何颜色遮盖住。
下面是源代码的下载链接。我原来以为这个代码直接复制就可以用了,但是考虑到一些朋友还是向我提出一些问题。我把原来项目的代码上传并发在这里。这个里面不仅有这个led“走马灯”效果,也包含我一些分形图形有关的代码。
(源代码下载):
https://files.cnblogs.com/hoodlum1980/FractalMaker.rar