摘 要:本文介绍了如何在VB中实现二维自适应坐标的绘制。根据显示区域大小和字体大小自适应地调整坐标,使各坐标标注不会发生重叠,也不会太疏散。其中的关键是自适应调整的算法,其原理同样适合于其它编程语言。
关键词:自适应,规范化,坐标标注
函数在数学中几乎无处不在,在许多与数学有关的应用软件中要把函数显示在坐标中,但是如何在显示各种不同的函数或者显示同一个函数的不同部分时保证坐标格及相应的坐标标注疏密得当美观大方呢?这就需要采取一定的自适应算法实现这种功能。可以说自适应也是人工智能的重要组成部分。结合编写的演示程序(参见图1),我们详述如下。
图1 演示程序界面
一、规范化坐标标注
这里的规范化坐标标注指的是坐标标注的数值以1、2、5、10、20、50等数中的一个为间隔的坐标标注法。我们可以看到这些数的特点是:要么其本身是10的某次幂,要么是10的某次幂的一半或五分之一。所以实际上0.01、0.02、0.05、0.1、0.2的小数也是可以作为规范化的坐标间隔。我们暂且称这些数为规范间隔数。
为了合理的规范化坐标间隔,在给定一个允许的最小坐标间隔StepMin (StepMin>0)后,我们要找到比StepMin大的并且最接近StepMin的一个规范间隔数。为此我们编了一个函数dFindStep(),源代码如下:
Private Function dFindStep(StepMin As Double) As Double
Dim dStep As Double '搜索比较值
Dim dStepOld As Double
If StepMin = 0 Then '若StepMin等于零则返回零
dFindStep = 0
Exit Function
End If
StepMin = Abs(StepMin) '若小于零则取其绝对值
dStep = 50 '从规范间隔数序列中的50开始搜索
If StepMin < dStep Then '若X<50,则不断往左搜索
While StepMin < dStep '此时dStep=50,5,0.5...等
dStepOld = dStep
dStep = dStep / 2.5 '此时dStep=20,2,0.2...等
If StepMin < dStep Then
dStepOld = dStep
dStep = dStep / 2 '此时dStep=10,1,0.1...等
If StepMin < dStep Then
dStepOld = dStep
dStep = dStep / 2 '此时dStep=5,0.5,0.05...等
End If
End If
Wend
dFindStep = dStepOld ' 因为dStep<StepMin<dStepOld,所以返回值为dStepOld
Else '若X>=50,则不断往右搜索
While StepMin > dStep '此时dStep=50,500,5000...等
dStep = dStep * 2 '此时dStep=100,1000,10000...等
If StepMin > dStep Then
dStep = dStep * 2 '此时dStep=200,2000,20000...等
If StepMin > dStep Then
dStep = dStep * 2.5 '此时dStep=500,5000,50000...等
End If
End If
Wend
dFindStep = dStep '返回dStep
End If
End Function
从上面的代码及其注释中很容易看到实现此函数的思路,在规范间隔数序列中从50开始向左或向右逐个与最小允许间隔StepMin比较,直到找到其右邻的规范间隔数为止。这个函数作为演示程序的示例函数供读者观察(参见图1)。这是一个具有自相似性质的超越函数。
二、自适应调整算法
VB提供很强的可视化编程环境,许多功能可以通过鼠标点击来实现,但是真正能灵活控制程序行为的还
是实实在在的程序代码,而且利用程序代码往往是程序更加高效。比如坐标的文本标注,很容易想到的是利用工具栏中的Lable控件,但事实上坐标标注文本的数目较多多少不定长度各异位置不同,并且他们随着被观察函数的变化而变化,即使用Lable控件数组也非常繁琐,而且要占用较多内存资源。采用Windows API函数TextOut在窗口中显示文本是一个较好的方法。其声明可从VB外接程序API Viewer中拷贝得到,具体如下:
Private Declare Function TextOut Lib "gdi32" Alias "TextOutA" _
(ByVal hdc As Long, ByVal X As Long, ByVal Y As Long, _
ByVal lpString As String, ByVal nCount As Long) As Long
Windows API 例程一般需要以像素为度量单位,TextOut函数中的参数X 、Y也不例外,但通常VB中缺省的单位是缇,我们可以从Screen对象(指整个 Windows 桌面)返回每一像素中水平 (TwipsPerPixelX属性) 或垂直 (TwipsPerPixelY属性)的缇数,这样就可以在缇和像素之间转换。
在具体的绘制过程中我们需要用到两个坐标,一个是与我们要表示函数相对应的物理坐标,一个是屏幕上的窗口坐标,窗口左上角的坐标为(0,0)。所以同一个点有两种坐标。下面介绍演示程序中具体的坐标绘制函数DrawCoordinate(),其源代码如下。其中的参数(iCrossX,iCrossY)为X轴和Y轴交点的窗口坐标,(dStartX,dStartY)为此交点的物理坐标,iWidth,iHeight为坐标轴表示范围的窗口坐标宽度和高度,dEndX,dEndY为X轴Y轴表示范围的上界。这个函数在名为picCoordinate的PictureBox控件中画坐标轴。
Private Sub DrawCoordinate( _
iCrossX As Integer, iCrossY As Integer, iWidth As Integer, iHeight As Integer, _
dStartX As Double, dStartY As Double, dEndX As Double, dEndY As Double)
Dim dCdnStep As Double '物理数值步长
Dim lCdnStep As Long '窗口数值步长
Dim crtPosition As Long '当前窗口坐标位置
Dim UpN As Double '最大格数
Dim dRemain As Double '第一个坐标格的物理坐标长度
Dim dValue As Double '当前物理坐标位置
Dim i As Integer
Dim strTemp As String '存储坐标标注文本
Dim strFormat As String '坐标标注文本的格式
Dim dHeight As Double '物理坐标高度
Dim dWidth As Double '物理坐标宽度
dHeight = dEndY - dStartY
dWidth = dEndX - dStartX
picCoordinate.Cls '对画图区域进行清屏
'画带有箭头的两根坐标轴
picCoordinate.Line (iCrossX, iCrossY)- _
(iCrossX + iWidth + 200, iCrossY), RGB(0, 0, 255)
picCoordinate.Line (iCrossX, iCrossY)- _
(iCrossX, iCrossY - iHeight - 200), RGB(0, 0, 255)
picCoordinate.Line (iCrossX, iCrossY - iHeight - 200)- _
(iCrossX - 30, iCrossY - iHeight), RGB(0, 0, 255)
picCoordinate.Line (iCrossX, iCrossY - iHeight - 200)- _
(iCrossX + 30, iCrossY - iHeight), RGB(0, 0, 255)
picCoordinate.Line (iCrossX + iWidth + 200, iCrossY)- _
(iCrossX + iWidth, iCrossY - 30), RGB(0, 0, 255)
picCoordinate.Line (iCrossX + iWidth + 200, iCrossY)- _
(iCrossX + iWidth, iCrossY + 30), RGB(0, 0, 255)
'接着画Y轴坐标格
'计算屏幕显示的像素限制下的坐标格最大数目
UpN = iHeight / Me.picCoordinate.TextHeight("8")
'计算自适应坐标格间隔大小
dCdnStep = dFindStep(dHeight / UpN)
'计算坐标格起始位置
dRemain = (Int(dStartY / dCdnStep) + 1) * dCdnStep - dStartY
If dRemain = dCdnStep Then dRemain = 0
dValue = dStartY + dRemain
lCdnStep = dCdnStep * iHeight / dHeight
crtPosition = iCrossY - dRemain * iHeight / dHeight
'计算自适应坐标间隔后的实际坐标格数目
UpN = Int(dHeight / dCdnStep)
If crtPosition - UpN * lCdnStep > iCrossY - iHeight Then UpN = UpN + 1
'根据坐标间隔大小选择不同的坐标值显示格式
If dCdnStep > 1 Then
strFormat = "######0"
Else
strFormat = "#####0.#####"
End If
'画坐标格
For i = 1 To UpN
picCoordinate.Line (iCrossX, crtPosition) _
-(iCrossX + 100, crtPosition), RGB(0, 0, 255)
strTemp = CStr(Format(dValue, strFormat))
TextOut picCoordinate.hdc, (iCrossX - 40 - Len(strTemp) * picCoordinate.TextWidth("8")) / Screen.TwipsPerPixelX, _
(crtPosition - picCoordinate.TextHeight("8") / 2) / Screen.TwipsPerPixelY, _
strTemp, Len(strTemp)
dValue = dValue + dCdnStep ' WaveScreen.Left - 100 ,
crtPosition = crtPosition - lCdnStep
Next i
'然后画X轴坐标格,方法步骤与画Y轴坐标类似
UpN = iWidth / Me.picCoordinate.TextWidth("8") / CInt(Abs(Log(dEndX - dStartX) / Log(10#)) + 4)
dCdnStep = dFindStep(dWidth / UpN)
dRemain = Int(dStartX / dCdnStep + 1) * dCdnStep - dStartX
If dRemain = dCdnStep Then dRemain = 0
dValue = dStartX + dRemain
lCdnStep = dCdnStep * iWidth / dWidth
crtPosition = iCrossX + dRemain * iWidth / dWidth
UpN = Int(dWidth / dCdnStep)
If crtPosition + UpN * lCdnStep < iCrossX + iWidth Then UpN = UpN + 1
If dCdnStep > 1 Then
strFormat = "######0"
Else
strFormat = "#####0.#####"
End If
For i = 1 To UpN
picCoordinate.Line (crtPosition, iCrossY) _
-(crtPosition, iCrossY - 100), RGB(0, 0, 255)
strTemp = CStr(Format(dValue, strFormat))
TextOut picCoordinate.hdc, (crtPosition - Len(strTemp) * picCoordinate.TextWidth("8") / 2) / Screen.TwipsPerPixelX, _
(iCrossY + 240 - picCoordinate.TextHeight("8")) / Screen.TwipsPerPixelY, _
strTemp, Len(strTemp)
dValue = dValue + dCdnStep
crtPosition = crtPosition + lCdnStep
Next i
End Sub
由上面可以看出,以Y轴为例自适应调整算法的思路是:先根据窗口高度和字符高度算出坐标格数目的上
限UpN,然后根据物理坐标高度和UpN算出允许的最小规范间隔,最后根据这个规范间隔和物理坐标起始值算出实际的坐标格位置和数目。对于X轴,不仅需要单个字符的宽度,还要估计坐标标注文本的字符个数,则可以用Cint(Abs(Log(dEndX - dStartX) / Log(10#)))估计标注文本长度,加上标注文本为小数时的第一个"0"和小数点,其误差最大为3,所以在其中加一个4就可以保证标注文本不会重叠。其它步骤与画Y轴坐标类似。
三、演示程序生成步骤
本演示程序用Windows98下的MicroSoft Visual Basic 6.0开发而成。具体步骤如下:
1. 进入VB6开发环境,打开新的标准工程(Standard EXE),工程名称可改为prjCdntDemo,Form1的Name改为frmMain,Caption为“自适应坐标演示:dFindStep()函数”,Width为6150, Height为4995。
2. 按图一布置9个CommandButton控件和一个PictureBox控件,各CommandButton的Name,Caption属性为:cmdXUp, 水平放大;cmdXDown, 水平缩小;cmdYUp, 垂直放大;cmdYDown, 垂直缩小;cmdLeft, 向左移动;cmdRight, 向右移动;cmdUp, 向上移动;cmdDown, 向下移动;cmdClose, 退出;PictureBox控件的Name为picCoordinate,Top为1020,Left为0,Width为6000,Height为3500。其它属性缺省。
3.在窗体代码的开头粘贴上TextOut的声明,在声明后头写上4个局部变量如下:
Private dStartX As Double '物理坐标水平起始值
Private dEndX As Double '物理坐标水平终止值
Private dStartY As Double '物理坐标垂直起始值
Private dEndY As Double '物理坐标垂直终止值
4. 然后粘贴上dFindStep(),DrawCoordinate()的源代码(见前文),最后写上如下事件相应函数代码:
Private Sub cmdClose_Click()
End '结束程序
End Sub
Private Sub cmdDown_Click()
Dim dStep As Double
dStep = (dEndY - dStartY) / 4
dStartY = dStartY + dStep
dEndY = dEndY + dStep
Call DrawAll
End Sub
Private Sub cmdLeft_Click()
Dim dStep As Double
dStep = (dEndX - dStartX) / 4
dStartX = dStartX + dStep
dEndX = dEndX + dStep
Call DrawAll
End Sub
Private Sub cmdRight_Click()
Dim dStep As Double
dStep = (dEndX - dStartX) / 4
dStartX = dStartX - dStep
dEndX = dEndX - dStep
Call DrawAll
End Sub
Private Sub cmdUp_Click()
Dim dStep As Double
dStep = (dEndY - dStartY) / 4
dStartY = dStartY - dStep
dEndY = dEndY - dStep
Call DrawAll
End Sub
Private Sub cmdXDown_Click()
dEndX = dStartX + (dEndX - dStartX) * 1.5
Call DrawAll
End Sub
Private Sub cmdXUp_Click()
dEndX = dStartX + (dEndX - dStartX) / 1.5
Call DrawAll
End Sub
Private Sub cmdYDown_Click()
dEndY = dStartY + (dEndY - dStartY) * 1.5
Call DrawAll
End Sub
Private Sub cmdYUp_Click()
dEndY = dStartY + (dEndY - dStartY) / 1.5
Call DrawAll
End Sub
Private Sub Form_Load()
dStartX = 0
dStartY = 0
dEndX = 1000
dEndY = 600
End Sub
Private Sub DrawAll()
Dim iCrossX As Integer '两坐标轴交点的窗口坐标X
Dim iCrossY As Integer '两坐标轴交点的窗口坐标Y
Dim iHeight As Integer '坐标轴的窗口坐标高度
Dim iWidth As Integer '坐标轴的窗口坐标宽度
Dim i As Integer
Dim dX As Double '示意波形的物理坐标X
Dim dY As Double '示意波形的物理坐标Y,或窗口坐标Y
iCrossX = 600
iCrossY = picCoordinate.Height - 500
iWidth = picCoordinate.Width - 1000
iHeight = picCoordinate.Height - 1000
Call DrawCoordinate(iCrossX, iCrossY, iWidth, iHeight, dStartX, dStartY, dEndX, dEndY)
'画示意波形,以本程序的dFindStep()函数为例
dY = dFindStep(CDbl(dStartX))
If dY < dStartY Then
dY = dStartY
ElseIf dY > dEndY Then
dY = dEndY
End If
picCoordinate.PSet (iCrossX, iCrossY - (dY - dStartY) * iHeight / (dEndY - dStartY)), RGB(0, 0, 255)
For i = iCrossX To iCrossX + iWidth Step 2
dX = dStartX + (i - iCrossX) * (dEndX - dStartX) / iWidth
dY = dFindStep(dX)
If dY >= dStartY And dY <= dEndY Then
picCoordinate.Line -(i, iCrossY - (dY - dStartY) * iHeight / (dEndY - dStartY)), RGB(255, 0, 0)
Else
If dY < dStartY Then
dY = dStartY
ElseIf dY > dEndY Then
dY = dEndY
End If
dY = iCrossY - (dY - dStartY) * iHeight / (dEndY - dStartY)
If picCoordinate.CurrentY <> dY Then
picCoordinate.Line -(i, dY), RGB(255, 0, 0)
Else
If dY = iCrossY - iHeight Then picCoordinate.PSet (i, dY), picCoordinate.BackColor
If dY = iCrossY Then picCoordinate.PSet (i, dY), RGB(0, 0, 255)
End If
End If
Next i
End Sub
Private Sub Form_Resize()
If Me.Height < 4000 Then Me.Height = 4000
picCoordinate.Width = Me.Width - 150
picCoordinate.Height = Me.Height - 1500
Call DrawAll
End Sub
Private Sub picCoordinate_Paint()
Call DrawAll
End Sub
这时就可以运行程序了。运行过程中可以点击命令按钮改变函数的观察范围或改变窗口大小观察坐标的自适应功能。
|