利用c++实现数值坐标刻度生成,并利用GDI绘制
给定两个数值如(3001,5020),如何做到均匀地标注刻度?
研究matlab图形刻度会发现,在matlab中,图像无论如何缩放,坐标刻度间隔都是以1,2,5为基数,按照10倍或者0.1倍的幂进行放大或缩小也即,刻度间隔为:
…0.1 0.2 0.5 ; 12 5; 1020 50; 100200 500; 10002000 5000….
负刻度也类似:
…-0.1 -0.2 -0.5 ; -1-2 -5; -10 -20 -50; -100 -200 -500; -1000 -2000 -5000….
在matlab图像放大中,还会发现,坐标轴刻度个数都在4个到10个之间。当数值超过1000或小于0.001,则会采用科学计数法。
从上面的刻度看,无论正刻度还是负刻度,相邻刻度最大倍数为2.5。如果把绘制的刻度最小个数控制为4,则最大个数为4*2.5=10;这即是为什么matlab绘制的刻度个数为什么不会超出4个到10个这个范围。
根据上面的分析,回到最初的问题,如何计算(3001,5020)均匀刻度,让刻度符合matlab的刻度规律。
首先确定寻找的刻度范围,限制最小刻度数4个,则最大刻度数为10个:
(5020-3001)/4=504.75
(5020-3001)/10=201.9
因而,201.9-504.75之间只有500这个刻度值符合matlab规律。下面是c++实现的刻度生成器clabelgenerator类
labelgenerator.h:
#ifndef _labelgenerator_h #define _labelgenerator_h #pragma once class clabelgenerator { public: clabelgenerator(void); ~clabelgenerator(void); bool generatelabel(float x1,float x2,int minlabelnum,int &reallabelnum,float *&label,cstringarray &labelarr,int &order,int _interal=0,int limitorder=3); // 产生坐标刻度 cstring stringcutzeros(cstring str); // 对标注字符串进行去零处理 }; #endif
labelgenerator.cpp:
//**************************** labelgenerator.cpp ***************************** // 包含功能:坐标刻度生成 // // 作者: jiangjp2812 1034378054@qq.com // 单位: 中国地质大学(武汉) // 日期: 2016/10/01 //************************************************************************************* #include "stdafx.h" #include "labelgenerator.h" #include "math.h" clabelgenerator::clabelgenerator(void) { } clabelgenerator::~clabelgenerator(void) { } bool clabelgenerator::generatelabel(float x1,float x2,int minlabelnum,int &reallabelnum, float *&label,cstringarray &labelarr,int &order,int _interal,int limitorder) // 生成坐标刻度 // x1,x2 :需要计算刻度的数值范围,minlabelnum:最小刻度数目。reallabelnum:得到的刻度个数。 // label:刻度的数值位置。labelarr:刻度字符串。order:刻度数值采用科学计数法的阶次 // _interal:指定刻度间隔,默认采用计算值。limitorder:超过此阶次采用科学计数,默认为3 { labelarr.removeall(); float labelinterval=0; // 标注间隔 float leftx=(x2-x1)/((float)minlabelnum*2.5); // 求标注间隔范围 if(leftx<0.000001) // 刻度产生失败 return false; float rightx=leftx*2.5; int kx=0,ky=0; float a=0; float done=0; // 1,2,5 不断乘以10判断是否在标注间隔范围内,如果在则终止循环, float dtwo=0; float dfive=0; //----------------- 计算x方向大于1刻度 ---------------------------------------------------------------------- while(a<=(x2-x1)) // 如果没有找到合适的标度,当超出最大数也终止 { done=1*pow((float)10,(float)kx); // 1,2,5 不断乘以10判断是否在标注间隔范围内,如果在则终止循环, dtwo=2*pow((float)10,(float)kx); dfive=5*pow((float)10,(float)kx); if( done>=leftx && done<=rightx) { labelinterval=done; break; } if(dtwo>=leftx && dtwo<=rightx) { labelinterval=dtwo; break; } if(dfive>=leftx && dfive<=rightx) { labelinterval=dfive; break; } kx++; a=dfive; // 每循环一次是1,2,5同时按倍数扩大,即判断的数扩大到a=done } //----------- x方向如果没有大于1刻度则计算小于1刻度 -------------------------------------- kx=0; a=1; if(labelinterval==0) { while(a>0.000001) // 如果没有找到合适的标度,当小于最小数也终止 { done=1*pow((float)0.1,(float)kx); // 1,2,5 不断乘以0.1判断是否在标注间隔范围内,如果在则终止循环, dtwo=2*pow((float)0.1,(float)kx); dfive=5*pow((float)0.1,(float)kx); if( done>=leftx && done<=rightx) { labelinterval=done; break; } if(dtwo>=leftx && dtwo<=rightx) { labelinterval=dtwo; break; } if(dfive>=leftx && dfive<=rightx) { labelinterval=dfive; break; } kx++; a=done; // 每循环一次是1,2,5同时按倍数缩小,即判断的数扩大到a=done } } //----------------------------------------------------------------------------------- if(labelinterval==0 ) return false; if(_interal!=0) labelinterval=_interal; // 如果指定间隔,则使用指定的间隔 float startx,endx; float temstartx/*,temendx*/; temstartx=x1/labelinterval-(int)(x1/labelinterval); if(temstartx==0) // 如果x1正好位于刻度上,则其实点从x1开始 { startx=labelinterval*(int)(x1/labelinterval); } else // 如果x1不位于刻度上,则起始点从x1之后某点开始 { if(x1>=0) startx=labelinterval*((int)(x1/labelinterval)+1); else startx=labelinterval*((int)(x1/labelinterval)-1+1); // 当出现负数时,取整向0靠拢,正半轴需加1,负半轴不需要 } if(x2>=0) endx=labelinterval*(int)(x2/labelinterval); else endx=labelinterval*((int)(x2/labelinterval)-1); // 当出现负数时,取整向0靠拢,正半轴需加1,负半轴不需要 reallabelnum=(endx-startx)/labelinterval+0.5+1; // 真实需要绘制的坐标刻度个数 // 此处(endx-startx)/labelinterval计算得到的数值应为整数,但浮点型计算得不到精确值 // 会略小于理论整数,因此需要加上0.5后再取整 label=new float[reallabelnum]; // 开辟坐标位置容器 int valueorder=0,intervalorder=0; // 采用科学计数法进行刻度标注,绝对值最大值阶数,标注间隔值阶数 float loop1=labelinterval,loop2=abs(endx); if(labelinterval>=1) // 求取标注间隔阶次,间隔大于1,正阶 { while(loop1>=10) { loop1=loop1/10; intervalorder++; } }else if(labelinterval<1) // 标注小于1,负阶 { while(loop1<0.1) { loop1=loop1*10; intervalorder++; } } float valueabs=abs(startx)>abs(endx) ? abs(startx) : abs(endx) ; // 获取最大值,求取阶数 int limitvmax=pow(10.0,limitorder); float limitvmin=pow(10.0,limitorder*-1); if(valueabs>=limitvmax) // 求取最大刻度值大于1000的阶次,正阶 { while(loop2>=10) { loop2=loop2/10; valueorder++; } }else if(valueabs<=limitvmin) // 求取最大刻度值小于0.001的阶次,负阶 { while(loop2<=0.1) { loop2=loop2*10; valueorder++; } } for(int i=0;i=limitvmax) // 如果绝对值最大值阶次大于3,则除以对应阶数量级 { str.format(_t("%.4f"),label[i]/pow(10.0,valueorder)); } else if(valueabs<=limitvmin) // 如果绝对值最大值阶次小于-3,则除以对应阶数量级 { str.format(_t("%.4f"),label[i]*pow(10.0,valueorder)); } str=stringcutzeros(str); // 对标注字符串进行尾部去零 labelarr.add(str); } if(valueorder==0) order=0; // 输出阶次 if(endx>=limitvmax) order=valueorder; if(endx<=limitvmin) order=-valueorder; return true; } cstring clabelgenerator::stringcutzeros(cstring str) // 对标注字符串进行去零处理 { cstring finalstr; finalstr.empty(); int length=str.getlength(); // 字符串长度 int pos=0; bool isdot=false; for(int i=0;i0) { if(isdot) finalstr=str.left(length-1-pos); else finalstr=str.left(length-1-(pos-1)); } else finalstr=str; return finalstr; }
类中有两个成员函数
boolgeneratelabel(float x1,float x2,int minlabelnum,int &reallabelnum,float*&label,cstringarray &labelarr,int &order,int _interal=0,intlimitorder=3); // 产生坐标刻度
cstringstringcutzeros(cstring str); // 对标注字符串进行去零处理
generaterlabel函数产生坐标刻度,其中x1,x2是输入的数值,minlabelnum是需要指定的最小刻度个数。如果生成matlab类似刻度则该数值为4,当然也可以设置其他值。reallabelnum是实际得到刻度个数(注意引用变量,通过引用变量把得到的数据输出),label是该处刻度的数值,labelarr是该处刻度对应的字符串,order是采用科学计数法输出的幂值,_interal是指定刻度值,给定初始值为0,即采用matlab相同刻度。如果不想采用此刻度,可以直接指定_interal值,则会采用指定的刻度生成标注值。limitorder是指定的科学计数法限制阶数,超过此阶数采用科学计数法。默认为3,即超过1000则采用科学计数法。
stringcutzeros是去除标注字符串尾部的0。因为生成的标注可能尾部有0,如0.3000或1.0。在matlab中就不会出现这样的显示方式。
函数调用:
int xlabelnum; float *pxlabel; cstringarray xlabelarr; int orderx; clabelgenerator labelgenerator;3001,5020 bool sucx=labelgenerator.generatelabel(3001,5020,4,xlabelnum,pxlabel,xlabelarr,orderx); // 产生x刻度和值刻度 if(pxlabel!=null) // 清空数组 { delete m_pxlabel; pxlabel=null; } bool sucy=labelgenerator.generatelabel(200001,5340000,5,xlabelnum,pxlabel,xlabelarr,orderx,0,4);坐标绘制采用gdi编写,被封装成clabeldrawer类
labeldrawer.h
#ifndef labeldrawer_h #define labeldrawer_h #pragma once class clabeldrawer { public: clabeldrawer(void); ~clabeldrawer(void); public: bool drawlabelhor(cdc *pdc, int xlabelnum,float *pxlabel,cstringarray &xlabelarr,int xorder, float desx1,float desx2,float desy1,float x1,float x2,cstring xtitle, bool isupper=true,colorref rgb=rgb(0,0,0)); bool drawlabelver(cdc *pdc, int ylabelnum,float *pylabel,cstringarray &ylabelarr,int yorder, float desy1,float desy2,float desx1,float y1,float y2,cstring ytitle, bool isleft=true,colorref rgb=rgb(0,0,0)); }; #endiflabeldrawer.cpp
//**************************** labeldrawer.cpp ***************************** // 包含功能:坐标刻度绘制(配合clabelgenerator类) // // 作者: jiangjp2812 1034378054@qq.com // 单位: 中国地质大学(武汉) // 日期: 2016/10/01 //********************************************************************************** #include "stdafx.h" #include "labeldrawer.h" #include clabeldrawer::clabeldrawer(void) { } clabeldrawer::~clabeldrawer(void) { } bool clabeldrawer::drawlabelhor(cdc *pdc, int xlabelnum,float *pxlabel,cstringarray &xlabelarr,int xorder, float desx1,float desx2,float desy1,float x1,float x2,cstring xtitle, bool isupper,colorref rgb) // 绘制水平刻度 // pdc: 绘图句柄。xlabelnum: 刻度数目。pxlabel:刻度真实数值。xlabelarr:刻度数值对应的字符串。 // xorder: 采用科学计数法阶次。desx1,desx2:标注绘制在窗口中的横坐标。desy1:水平刻度在窗口中的纵坐标。 // xtitle: 坐标轴名称字符串。isupper:若为true,刻度位于刻度线上方,否则位于刻度线下方 // rgb:刻度颜色。 { if(abs(desx2-desx1) <0.0001) return false; if(pxlabel==null) return false; pdc->setbkmode(transparent); pdc->settextcolor(rgb); cfont font,fontunit,*oldfont; // 创建times new roman字体 font.createpointfont(100,_t("times new roman"),pdc); fontunit.createpointfont(120,_t("times new roman"),pdc); cpen pen(0,1,rgb),*oldpen; oldpen=pdc->selectobject(&pen); float posx,posy; oldfont=pdc->selectobject(&font); textmetric tm; pdc->gettextmetrics(&tm); long tw=tm.tmavecharwidth; // 获取输出字符信息,主要是为了使得短线标注对准字符串中间位置 long th=tm.tmheight; // 用于计算字符相对短线输出位置 double k=(desx2-desx1)/(x2-x1); if(isupper==true) // 标注在上边 { pdc->selectobject(&font); pdc->settextalign(ta_center | ta_bottom); for(int i=0;imoveto(posx,desy1); pdc->lineto(posx,desy1-tw); pdc->textoutw(posx,desy1-tw-1,xlabelarr.getat(i)); } posx=k*((x1+x2)/2.0-x1)+desx1; pdc->selectobject(&fontunit); pdc->textoutw(posx,desy1-tw-th,xtitle); // 绘制x方向坐标单位 } else{ // 标注在下边 pdc->selectobject(&font); pdc->settextalign(ta_center | ta_top); for(int i=0;imoveto(posx,desy1); pdc->lineto(posx,desy1+tw); pdc->textoutw(posx,desy1+tw+1,xlabelarr.getat(i)); } posx=k*((x1+x2)/2.0-x1)+desx1; pdc->selectobject(&fontunit); pdc->textoutw(posx,desy1+tw+th,xtitle); // 绘制x方向坐标单位 } if(xorder!=0) // 绘制x方向阶次 { cfont font1,font2; font1.createpointfont(100,_t("times new roman"),pdc); font2.createpointfont(70,_t("times new roman"),pdc); float fontrate=100.0/70.0; cstring tenstr("×10"),orderstr; // ×10字符和阶次字符 orderstr.format(_t("%d"),xorder); if(isupper==true) { pdc->selectobject(&font1); pdc->settextalign(ta_bottom | ta_right); pdc->textoutw(desx2-tw/fontrate*2,desy1-tw-th,tenstr); pdc->selectobject(&font2); pdc->settextalign(ta_bottom | ta_left); pdc->textoutw(desx2-tw/fontrate*2,desy1-tw-th-th/2.0,orderstr); } else { pdc->selectobject(&font1); pdc->settextalign(ta_top | ta_right); pdc->textoutw(desx2-tw/fontrate*2,desy1+tw+th+th/2.0,tenstr); pdc->selectobject(&font2); pdc->settextalign(ta_top | ta_left); pdc->textoutw(desx2-tw/fontrate*2,desy1+tw+th,orderstr); } } pdc->selectobject(oldfont); pdc->selectobject(oldpen); return true; } bool clabeldrawer::drawlabelver(cdc *pdc, int ylabelnum,float *pylabel,cstringarray &ylabelarr,int yorder, float desy1,float desy2,float desx1,float y1,float y2,cstring ytitle, bool isleft,colorref rgb) // 绘制垂直刻度 // pdc: 绘图句柄。ylabelnum: 刻度数目。pylabel:刻度真实数值。ylabelarr:刻度数值对应的字符串。 // yorder: 采用科学计数法阶次。desy1,desy2:标注绘制在窗口中的纵坐标。desx1:垂直刻度在窗口中的横坐标。 // ytitle: 坐标轴名称字符串。isleft:若为true,刻度位于刻度线左边,否则位于刻度线右边 // rgb:刻度颜色。 { if(abs(desy2-desy1)<0.0001) return false; if(pylabel==null) return false; pdc->setbkmode(transparent); pdc->settextcolor(rgb); cfont font,fontunit,*oldfont; // 创建times new roman字体 font.createpointfont(100,_t("times new roman"),pdc); fontunit.createpointfont(120,_t("times new roman"),pdc); cpen pen(0,1,rgb),*oldpen; oldpen=pdc->selectobject(&pen); float posx,posy; oldfont=pdc->selectobject(&font); textmetric tm; pdc->gettextmetrics(&tm); long tw=tm.tmavecharwidth; // 获取输出字符信息,主要是为了使得短线标注对准字符串中间位置 long th=tm.tmheight; // 用于计算字符相对短线输出位置 int charlength=0; for(int i=0;icharlength) charlength=ylabelarr.getat(i).getlength(); } charlength=charlength+1; double k=(desy2-desy1)/(y2-y1); if(isleft==true) // 标注左边 { pdc->selectobject(&font); pdc->settextalign(ta_bottom | ta_right); for(int i=0;imoveto(desx1,posy); pdc->lineto(desx1-tw,posy); pdc->textoutw(desx1-tw-1,posy+th/2.0,ylabelarr.getat(i)); } pdc->settextalign(ta_bottom | ta_center); // 纵向字体按纵向的bottom和left设置,即和横向颠倒 posy=k*((y1+y2)/2.0-y1)+desy1; logfont logfont; fontunit.getlogfont(&logfont); logfont.lfescapement =900; cfont fontemp; fontemp.createfontindirect(&logfont); pdc->selectobject(&fontemp); pdc->textoutw(desx1-tw*charlength-tw,posy,ytitle); } else{ pdc->selectobject(&font); pdc->settextalign(ta_bottom | ta_left); for(int i=0;imoveto(desx1,posy); pdc->lineto(desx1+tw,posy); pdc->textoutw(desx1+tw+1,posy+th/2.0,ylabelarr.getat(i)); } pdc->settextalign(ta_top | ta_center); posy=k*((y1+y2)/2.0-y1)+desy1; logfont logfont; fontunit.getlogfont(&logfont); logfont.lfescapement =900; cfont fontemp; fontemp.createfontindirect(&logfont); pdc->selectobject(&fontemp); pdc->textoutw(desx1+tw*charlength+tw,posy,ytitle); } if(yorder!=0) { cfont font1,font2; // 创建times new roman字体 font1.createpointfont(100,_t("times new roman"),pdc); font2.createpointfont(70,_t("times new roman"),pdc); float fontrate=100.0/70.0; cstring tenstr("×10"),orderstr; orderstr.format(_t("%d"),yorder); if(isleft==true) { pdc->selectobject(&font1); pdc->settextalign(ta_bottom | ta_right); pdc->textoutw(desx1-charlength*tw-tw-tw/fontrate*2,desy2,tenstr); pdc->selectobject(&font2); pdc->settextalign(ta_bottom | ta_left); pdc->textoutw(desx1-charlength*tw-tw-tw/fontrate*2,desy2-th/2.0,orderstr); } else { pdc->selectobject(&font1); pdc->settextalign(ta_bottom | ta_left); pdc->textoutw(desx1+charlength*tw+tw,desy2,tenstr); pdc->selectobject(&font2); pdc->settextalign(ta_bottom | ta_left); pdc->textoutw(desx1+charlength*tw+tw+(tenstr.getlength()+1)*tw,desy2-th/2.0,orderstr); } } pdc->selectobject(oldfont); pdc->selectobject(oldpen); return true; }
clabeldrawer类中有两个成员函数,drawlabelhor用于绘制水平刻度,drawlabelver用于绘制垂直刻度。函数参数在代码中有说明,这个类必须配合上边的clabelgenerator类使用。下面是使用实例。
首先利用vs生成一个mfc单文档程序,为了方便说明,直接把数据放在了ondraw函数中:
void clabeltestview::ondraw(cdc* pdc) { clabeltestdoc* pdoc = getdocument(); assert_valid(pdoc); if (!pdoc) return; // todo: 在此处为本机数据添加绘制代码 int labelnum; float *plabel=null; cstringarray labelarr; int order; float v1=-0.0005,v2=-0.0003; float desx1=100,desx2=500,desy1=100,desy2=500; // 生成数值(-0.0005 -0.0003)范围 clabelgenerator labelgen; labelgen.generatelabel(v1,v2,4,labelnum,plabel,labelarr,order); clabeldrawer labeldraw; // 绘制数值(-0.0005 -0.0003)范围上刻度 labeldraw.drawlabelhor(pdc,labelnum,plabel,labelarr,order,desx1,desx2,desy1,v1,v2,_t("upper label")); // 绘制数值(-0.0005 -0.0003)范围下刻度 labeldraw.drawlabelhor(pdc,labelnum,plabel,labelarr,order,desx1,desx2,desy2,v1,v2,_t("lower label"),false,rgb(0,0,255)); // 绘制数值(-0.0005 -0.0003)范围左刻度 labeldraw.drawlabelver(pdc,labelnum,plabel,labelarr,order,desy1,desy2,desx1,v1,v2,_t("left label")); // 绘制数值(-0.0005 -0.0003)范围右刻度 labeldraw.drawlabelver(pdc,labelnum,plabel,labelarr,order,desy1,desy2,desx2,v1,v2,_t("right label"),false,rgb(255,0,0)); // 绘制矩形框 pdc->selectstockobject(null_brush); pdc->rectangle(desx1,desy1,desx2,desy2); v1=30; v2=803; desx1=650;desx2=950;desy1=100;desy2=500; if(plabel!=null) delete plabel; // 绘制数值(30 803)范围刻度 labelgen.generatelabel(v1,v2,5,labelnum,plabel,labelarr,order); // 绘制数值(30 803)范围水平上刻度 labeldraw.drawlabelhor(pdc,labelnum,plabel,labelarr,order,desx1,desx2,desy1,v1,v2,_t("hor axis"),true,rgb(0,0,255)); pdc->moveto(desx1,desy1); pdc->lineto(desx2,desy1); // 绘制数值(30 803)范围水平上逆向刻度 labeldraw.drawlabelhor(pdc,labelnum,plabel,labelarr,order,desx2,desx1,desy1+50,v1,v2,_t("reverse hor axis"),true,rgb(0,0,255)); pdc->moveto(desx1,desy1+50); pdc->lineto(desx2,desy1+50); desx1=700;desx2=1000;desy1=200;desy2=550; // 绘制数值(30 803)范围垂直左刻度 labeldraw.drawlabelver(pdc,labelnum,plabel,labelarr,order,desy1,desy2,desx1,v1,v2,_t("ver axis"),true,rgb(255,0,255)); pdc->moveto(desx1,desy1); pdc->lineto(desx1,desy2); // 绘制数值(30 803)范围垂直左逆向刻度 labeldraw.drawlabelver(pdc,labelnum,plabel,labelarr,order,desy2,desy1,desx1+100,v1,v2,_t("reverse ver axis"),true,rgb(255,0,255)); pdc->moveto(desx1+100,desy1); pdc->lineto(desx1+100,desy2); }生成的效果如图
上一篇: 我一定要杀了你