hoodlum1980 ( 發發 ) 的技术博客

——ZJU: 百十载,求是魂,求是创新(领域:c/c++, c#, .NET, Compact Framework, WinCE,Matlab,PhotoShop,数字图像处理,电力系统自动化,继电保护)
posts - 48, comments - 54, trackbacks - 0, articles - 1

2007年7月6日

这里延续讲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可以按照如下的下标访问:
//像素(i,j)
byte* p=(byte*)(void*)bmData.Scan0;
p[ stride
*+ i*3 ]=(byte);//Blue channel
p[ stride*+ i*3 +1 ]=(byte);//Green channel
p[ stride*+ 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*+ m*3 ]=pSrc[ strideSrc*+ i*3];        //B
53                        p[ stride*+ m*3 +1 ]=pSrc[ strideSrc*+ i*3+1];    //G
54                        p[ stride*+ m*3 +2 ]=pSrc[ strideSrc*+ 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;

对于vs.net中的图片编辑器来说,它相当于celldistance=1的情况,并且在cell的间隔之间绘制了Grid,我发现这些grid线是由两种颜色交替而组成的,因此这可以保证Grid线不会被任何颜色遮盖住。

下面是源代码的下载链接。我原来以为这个代码直接复制就可以用了,但是考虑到一些朋友还是向我提出一些问题。我把原来项目的代码上传并发在这里。这个里面不仅有这个led“走马灯”效果,也包含我一些分形图形有关的代码。
(源代码下载):
http://www.cnblogs.com/Files/hoodlum1980/FractalMaker.rar


posted @ 2007-07-06 21:16 hoodlum1980 阅读(274) | 评论 (2)编辑

Led控件,可能是非常经典和常用的了,但是很遗憾的是,这个名称至少涵盖了三种控件:
1.是7段式的有发光二极管构成的Led,通常用来显示数字。
2.是指示灯,通常用来闪烁,指示电源,等状态。
3.是由发光二极管阵列组成的模拟显示屏,这种led屏有较高的分辨率,所以可以显示中文内容和一定容量的界面。

这篇文章里面说的是1.其中2这种在codeproject上面有很多例子,我曾经改写其中的例子成为在移动设备上使用。

今天我用c#写了这样一个Led控件。我也曾经下载过,可惜好像没有什么源码,想来这个东西应该没太复杂,所以干脆自己也写个玩玩。记得当年本科时候我就写过单片机程序,控制led数码管,实现了时间调节时闪烁,“霓虹灯”屏保等效果,大大出乎了老师的预期。我想作为一个控件,这个东西的主要功能是为了模拟现实中的用户界面,或者让它更美观,更有趣味一些。而如果只是为了显示一些信息,显然有太多其他的更方便的选择了。所以我用较短的时间实现了一个功能比较简陋的但是能用的控件。

它的运行效果如下:我想这个没什么可说的。


笔画变细以后是这样的:


这里我加载了6个控件,一个定时器,用来实时的显示系统时间。
对于这个控件我想了一下,也许可以使用图片资源来做,比较方便,但我还是把它做成了矢量型的,这样,把一个led中所有笔画(我称为section,段)采用一个六边形模拟,我需要随时能够计算出所有笔画的坐标,这样一个led具有6个点*7段=42个点,这样会占用300多bytes左右。如果显示的数字不多,还是可以不去在乎这点内存的。这就是矢量图的特点,如果你想表现的更细腻,显然会极大加大计算量,脑子也会累的。

然后我用下面的函数计算出七段的坐标:
/// <summary>
/// 重新计算段的坐标!!!(这种方法得出的图形将是矢量的,不受缩放影响)
/// </summary>

private void ComputeSections(int ledwidth,int ledheight)
{
    
//计算出控件中心点的坐标
    int cx=ledwidth/2;
    
int cy=ledheight/2;

    
int t1=this.m_SectionThick*3/4;    //大斜坡长
    int t2=this.m_SectionThick/4;    //小斜坡长
    int t3=this.m_SectionThick/2;    //中斜坡长
    
//段的一半长度!
    int hw=cx-this.m_SectionThick-2;    //half width of section 距离边缘2像素
    int hh=cy-this.m_SectionThick-2;    //half height of section
    Section[] s=this.m_Sections;

    
//第0段(最底下一横)
    s[0].P[0].X=cx-hw-this.m_SectionThick/4;
    s[
0].P[0].Y=cy+hh+this.m_SectionThick/4;
    s[
0].P[1].X=s[0].P[0].X-t2;
    s[
0].P[1].Y=s[0].P[0].Y-t2;
    s[
0].P[2].X=s[0].P[1].X+t1;
    s[
0].P[2].Y=s[0].P[1].Y-t1;

    
//第1段(它是中间的一横,因为和其他任何段都没对称关系,只能手写!)
    s[1].P[0].X=cx-hw+this.m_SectionThick*3/16;
    s[
1].P[0].Y=cy+t3;
    s[
1].P[1].X=s[1].P[0].X-t3;
    s[
1].P[1].Y=s[1].P[0].Y-t3;
    s[
1].P[2].X=s[1].P[0].X;
    s[
1].P[2].Y=cy-t3;

    
//第2段(最上面一横,与第0段按y轴对称)
    for(int i=0;i<3;i++)
    
{
        s[
2].P[i].X=s[0].P[2-i].X;
        s[
2].P[i].Y=ledheight-s[0].P[2-i].Y;
    }

    
//循环为0,1,2三个水平段的p[3],p[4],p[5]赋值,注意这几个值可以根据钱三个点求出
    for(int i=0;i<3;i++)
    
{
        
for(int j=3;j<6;j++)
        
{
            s[i].P[j].X
=ledwidth-s[i].P[5-j].X;
            s[i].P[j].Y
=s[i].P[5-j].Y;
        }

    }

    
//到这里我们已经计算好了0,1,2段的全部坐标,下面开始计算3~6段,他们具有相互对称的关系!
    
    
//第3段(左上的竖)(注意本身自己也不具备对称关系,6个点都要手写)
    s[3].P[0].X=cx-hw+this.m_SectionThick/5;
    s[
3].P[0].Y=cy-this.m_SectionThick*3/5;
    s[
3].P[1].X=s[3].P[0].X-t3;
    s[
3].P[1].Y=s[3].P[0].Y+t3;
    s[
3].P[2].X=s[3].P[1].X-t3;
    s[
3].P[2].Y=s[3].P[1].Y-t3;
    s[
3].P[3].X=s[3].P[2].X;
    s[
3].P[3].Y=s[3].P[0].Y-hh+this.m_SectionThick;
    s[
3].P[4].X=s[3].P[3].X+t2;
    s[
3].P[4].Y=s[3].P[3].Y-t2;
    s[
3].P[5].X=s[3].P[4].X+t1;
    s[
3].P[5].Y=s[3].P[4].Y+t1;

    
//计算4,5,6段的点坐标(4和3段x对称,5和3是y对称,6和3是原点对称)
    for(int i=0;i<6;i++)
    
{
        
int m=(8-i)%6;
        s[
4].P[i].X=ledwidth-s[3].P[m].X;
        s[
4].P[i].Y=s[3].P[m].Y;

        s[
5].P[i].X=s[3].P[m].X;
        s[
5].P[i].Y=ledheight-s[3].P[m].Y;

        s[
6].P[i].X=ledwidth-s[3].P[i].X;
        s[
6].P[i].Y=ledheight-s[3].P[i].Y;
    }

}


上面的代码可能是这个控件里唯一复杂的工作。。。。这是一个很劳累的工作,可是我暂时没想到更好的办法让它更加简化。段的编号顺序是:0底部横,1中部横,2顶部横,3左上竖,4右上竖,5左下竖,6右下竖,7小数点(我暂时没有绘制它)。

显示时,使用一个byte来控制,其所在位为1时,相应的section被点亮,否则为熄灭。
因此,几个基本数字的编码如下:
(byte)0x7d,//0
(byte)0x50,//1
(byte)0x37,//2
(byte)0x57,//3
(byte)0x5a,//4
(byte)0x4f,//5
(byte)0x6f,//6
(byte)0x54,//7
(byte)0x7f,//8
(byte)0x5f,//9
(byte)0x02,//-

最后,当我们显示时:
//绘制七段,section是一个struct,包含一个point数组
for(int i=0;i<this.m_Sections.Length;i++)
   {
    
if((this.m_DisplayCode & (1<<i))!=0)
         {
               
this.m_Brush.Color=this.ForeColor;    
               g.FillPolygon( this.m_Brush,
this.m_Sections[i].P);
    }

    
else
         
{
                
this.m_Brush.Color=this.m_OffColor;
                g.FillPolygon(
this.m_Brush, this.m_Sections[i].P); 
       }

}

对外部可以提供一个简单的属性,displaynumber来获取和设置显示的数字,允许0~9.
也允许外部设置笔画宽度,这样上面的sections坐标需要重新计算,并更新到显示。但是这个属性不能设置的过大,否则坐标值相互超越则显示会出错。

最后我想了一下,目前它的灵活性被我怀疑,加载了6个一摸一样的led控件也让我感到使得代码很笨拙。它现在的功能比较简陋,它可以继续扩展,使他将来能够同时容纳多个显示位。

在最后我提供这个控件以及示例的完整代码下载地址:(当然,它目前还不成熟,在结构和接口上有进一步的进化空间)

http://www.cnblogs.com/Files/hoodlum1980/LedTest_ByHoodlum.rar

posted @ 2007-07-06 16:21 hoodlum1980 阅读(257) | 评论 (0)编辑

我觉得这真是一本不错的书,正是符合我的喜好的一本书。恩,所以我还是买了原版回来,尽管我的英文水平有些差劲。我注意到有网上一个人评论说,里面有些雕虫小技而已,对于大的项目可能不适用,或者在现代计算机内存,cpu速度都很强的情况,有些问题不那么在乎了。
我对这个评价觉得很不满意,尤其是将“智慧”所称雕虫小技,更是感到不认可。我觉得这本书想讲解的其实不是投机取巧的东西,而是完全为了是锻炼程序员的思想层次上的东西。这些也是决定一个程序员素质高低的关键。
技术可能会更新,涌现新的语言,工具和方法,环境等等,但是思想则会更持久,而这本书主要叙述的是思想,视角,从这个层次上提高效率和优美性。

我也看到了另一本书,是effective系列中的effective c#,我觉得这本书也是我比较需要的,因为里面有一些问题可能我并不是非常清楚。它主要的是帮助不太了解这些问题的人了解这些问题,了解编译器的喜好和个性,从而让你更有效的使用这个IDE,让你了解影响你的代码效率的比较底层的对你透明的一些东西,从而可写出更符合编译器口味,更有效率的代码。这和思想和算法层次是不同的,这主要是从高级语言特性等方面来提高代码效率,教给你良好的习惯和摒弃不良的代码习惯。它的层次更接近应用层,与语言,ide特性更相关。

而pearls是比较抽象的,我在书中看到的叙述感到非常有趣,因为作者提出问题后经常会给这个问题实际中的例子,这就非常有信服力,让你觉得这些智慧确实有用!我很喜欢,因为这些绝非投机,而非小计,而是智慧!不然,你觉得算法是什么,它就是一种抽象的方法,一种很强的智慧操纵后的解决方法转换处理(从人的直观型逻辑思维习惯转换为代码的过程控制型习惯),一种智慧的结晶。

这本书里面提到另一个人提出了一句话,而这句话恰恰也是作者的想法:A problem that seems difficult may have a simple, unexpected solution. 我想我确实是不太信任别人的翻译的,所以我要买回原版。因为中文表义具有一种模糊性,就是模棱两可,而英文表义具有一种确定性,即它的一些动词的语态与时间,被动等等结合,它可以更准确无歧义的进行描述。而且中文译者里面的素质也是良莠不齐的,可能得到公认和信任的也就是侯捷先生了,从他在前面列出一系列的词语对照和原因解释就能看出他的责任心和工作态度。回过来这句话,一个看起来很困难的问题,可能具有一个简单的出人意料的解法。嗯,我想这句话来描述优美的算法和代码非常恰当!

——————————————
比如说我在88上遇到的这个问题:
1,1,2,2,3,3,4,4,5,5这十个数字,要求排成一排,是两个1之间有1个数字,两个2之间有2个数字,...,两个5之间有5个数字。

怎么排呢?你可以在纸上尝试一下,无论如何,你都做不出结果,因为这道题无解。
怎么证明它无解呢?老实说,我虽然看到提示说用奇偶性证明,但是我没有想出怎么回事,而是用tc遍历搜索了一下发现没有任何输出。证明的方法我觉得这也可以说是另一种智慧能力的体现了,但是其实过程很简单:如果给10个数字从1到10进行连续编号,则显然这组编号必定由5奇数5偶数组成。
我们假设具有一个解满足题目要求,那么:
1*1,3***3,5*****5,  这三对的数字的编号奇偶性相同。也就是(奇奇,或偶偶)*3
2**2, 4****4, 这两对数字的奇偶性不同。也就是说(是1奇1偶)*2
可见,解的编号具有偶数个偶数和偶数个奇数,和5奇5偶矛盾!因此无解。

end~

posted @ 2007-07-06 10:33 hoodlum1980 阅读(190) | 评论 (0)编辑