จากการที่ได้เผยแพร่บทความเรื่อง “โปรแกรมตัวอย่าง การวาดและหมุน จานคำนวณ 360 องศา (Delphi)” มาตั้งแต่เปิดเว็บใหม่ๆ เมื่อหลายปีก่อน ข้อจำกัดของโปรแกรมที่ได้คือขนาดของจานคำนวณรวมถึงขนาดของภาพดวงในโปรแกรมจะยังจำกัดไม่สามารถขยายตามโปรแกรมเวลาที่มีการลากเม้าส์ขยายหน้าต่างหรือคลิก Maximize ซึ่งเวลานั้นดูเหมือนจะไม่ใช่เรื่องใหญ่เพราะ Resolution ของจอคอมพิวเตอร์จะอยู่ที่ 800X600 กันเป็นส่วนใหญ่ แต่แล้วความเปลี่ยนแปลงด้านเทคโนโลยีทำให้ขนาดหน้าจอคอมพิวเตอร์มีความเปลี่ยนแปลงเพิ่มขึ้นและหลากหลายขึ้น จากเดิมเครื่องคอมพิวเตอร์ตั้งโต๊ะหน้าจอสัดส่วน 4:3 จาก Resolution 800X600 มาเป็น 1024X768, 1280X1024 ฯลฯ แล้วยังมีจอ Wide Screen สัดส่วน 16:9 อีกหลาย Resolution ซึ่งเครื่อง Notebook/Netbook รุ่นใหม่ๆ ต่างก็ใช้จอแบบ Wide Screen 16:9 แทนจอ 4:3 กันแทบจะทั้งหมดแล้ว ยังไม่นับรวมอุปกรณ์มือถือ/Tablet ที่คงจาระไนกันไม่หวาดไม่ไหว
ในที่นี้มาคุยกันเฉพาะเรื่องการเขียนโปรแกรมบน Windows ด้วย Delphi นะครับ ถ้าใครเข้าใจแล้วจะสามารถเอาไปประยุกต์ใช้กับการเขียนโปรแกรมภาษาอื่นบนระบบปฏิบัติการอื่นหรืออุปกรณ์อื่นได้อย่างไรก็ขออนุโมทนาสาธุไว้ล่วงหน้า จากบทความ “โปรแกรมตัวอย่าง การวาดและหมุน จานคำนวณ 360 องศา (Delphi)” เมื่อวาดจานคำนวณหรือจานหมุนสำหรับโหราศาสตร์ยูเรเนียนได้แล้ว สั่งให้หมุนได้ก็แล้ว เมื่อหน้าจอมันใหญ่ขึ้นจนจานคำนวณที่วาดไว้มันเล็กเกินไป ทำยังไงจะให้มันใหญ่ขึ้นตามส่วนแล้วยังสามารถหมุนได้เหมือนเดิม?
หลักการง่ายๆ ที่ผมเองมองข้ามมาเป็นปีๆ ก็คือเรื่องของสัดส่วนนั่นเองครับ ถ้าภาพมันใหญ่ขึ้น 2 เท่า 3 เท่า ค่าต่างๆ ที่เป็นขนาดของจานคำนวณก็ต้องคูณ 2 คูณ 3 เข้าไป ถ้าค่าที่เพิ่มขึ้นเป็นทศนิยม เช่น 1.2, 1.5, 2.3, 3.7 ฯลฯ ก็เอาค่านั้นๆ คูณเข้าไป เมื่อได้หลักการง่ายๆ แล้ว ลงมือทำจริงๆ จะง่ายหรือเปล่า เดี๋ยวมาดูกันต่อไป
เหตุหนึ่งที่ทำให้เกิดการมองข้ามหลักง่ายๆ ดังที่ว่า ส่วนหนึ่งเกิดจากอาการดิ้นได้ของการใช้ Object TImage ในการเขียนโปรแกรม Delphi ครับ คือถ้าเราตั้งค่า Property Align ของมันเป็น AlClient แล้วพอมีการขยายหน้าต่างโปรแกรมเจ้า Image ที่ใช้เป็นพื้นการวาดภาพจานคำนวณมันก็จะขยายตามโดยอัตโนมัติ แต่เป็นการขยายออกตามประสาซื่อๆ ของมัน เมื่อไม่ได้เป็นการขยายจากการสั่งการควบคุมที่ถูกต้องของเรา เส้นต่างๆ ในภาพมันจะหนาขึ้น และไม่สามารถหมุนจานคำนวณได้ครับ
การกำหนดขนาดความกว้างความสูงของภาพดวงชะตาที่จะวาดก็ไม่ใช่ไปกำหนดที่ Property Width และ Height ของ Image นะครับ แต่ต้องกำหนดที่ Image1.Picture.Graphic.Width และ Image1.Picture.Graphic.Height ดูแล้วออกจะซับซ้อนสักนิด เปรียบไปแล้ว Image เป็นเหมือนกรอบรูปภาพ ส่วน Picture.Graphic เป็นเหมือนตัวภาพจริง ตัวภาพอาจจะเล็กกว่ากรอบหรือใหญ่จนล้นกรอบก็ได้ ตรงนี้ถ้าท่านไม่เข้าใจเพราะยังใหม่กับ Delphi หรือเพราะผมเขียนไม่รู้เรื่องเองก็คอยดูจาก Source Code ในตอนถัดไป อาจจะลองแก้ Source Code แล้ว Run โปรแกรมทดสอบดูว่าผลมันต่างกันอย่างไรนะครับ
หัวใจสำคัญยังคงอยู่ที่การกำหนดขนาดของ Form และ Image ที่จะใช้ โดยในส่วนของ Form ได้กำหนดค่า Property Constraints ไว้ดังนี้ครับ
Constraints.MaxHeight = 750
Constraints.MaxWidth = 676
Constraints.MinHeight = 451
Constraints.MinWidth = 377
หมายความว่าขนาดของ Form ซึ่งก็คือขนาดของหน้าต่างโปรแกรมที่จะ Run จริง จะมีค่าสูงสุดไม่เกิน 750X676 ต่ำสุดไม่เกิน 451X377 ซึ่งความสูงจะมากกว่าความกว้างเนื่องจากผมใส่ Object TPanel เอาไว้ด้วย
และใน Event FormResize มีการกำหนดค่าไว้ดังนี้ครับ
Image1.Width := Form1.Width-16; Image1.Picture.Graphic.Width := Image1.Width;
Image1.Height := Form1.Height-90; Image1.Picture.Graphic.Height := Image1.Height;
If Image1.Width < Image1.Height then ChartSize := Image1.Width Else ChartSize := Image1.Height;
นั่นคือเมื่อมีการปรับขนาดหน้าต่างโปรแกรมแล้ว Image1 จะมีความกว้างเท่ากับความกว้างของ Form ลบด้วย 16 ความสูงเท่ากับของ Form ลบด้วย 90 ซึ่งโอกาสที่จะเท่ากันเป๊ะเป็นสี่เหลี่ยมจัตุรัสนั้นน้อยมาก แต่ในการวาดจานคำนวณต้องใช้พื้นที่เป็นจัตุรัสเพื่อให้วงกลมกลมจริงๆ จึงมีการเลือกค่าที่น้อยกว่ามาเป็นตัวกำหนดขนาดของภาพดวงชะตา (ChartSize) ที่จะใช้วาดจานคำนวณ
จากนั้นหาว่าขนาดของภาพดวงชะตาที่เปลี่ยนแปลงไปมันมากกว่าค่าเริ่มต้นของเราทีแรกเป็นสัดส่วนเท่าไหร่
Ratio := ChartSize/361;
ค่าเรโช (Ratio) นี้ เป็นตัวแปรใหม่ที่เพิ่มเข้ามาใน Procedure การวาดภาพจานคำนวณ ซึ่งในตอนแรกสุดมันจะมีค่าเป็นหนึ่ง พอมันมีค่าเปลี่ยนไปจากการยืดหดหน้าต่างโปรแกรม มันจะเป็นตัวช่วยในการรักษาสัดส่วนของจานคำนวณให้ถูกต้องและยังคงหมุนได้อยู่เสมอ
ทีนี้มีใครสงสัยไหมครับว่าเมื่อก่อนหน้านี้ทำไมถึงมีการกำหนดขนาดสูงสุดของ Form1 หรือขนาดของหน้าต่างโปรแกรมไม่ให้เกิน 750X676 ? ทำไมไม่กำหนดแต่ขนาดต่ำสุด แล้วจะกว้างจะสูงออกไปแค่ไหนก็ได้?
ก็เพราะว่าเวลาทดลองจริงผมต้องพบกับปัญหาที่ยังแก้ไม่ตก คือถ้าไม่กำหนดขนาดใหญ่สุดไว้ เมื่อหน้าต่างโปรแกรมขยายใหญ่กว่า 750X676 หรือภาพดวงที่จะใช้วาดจานคำนวณมีขนาดใหญ่กว่า 660X660 แล้ว จานคำนวณจะหมุนไม่ไปครับ ถ้าเป็น 660X660 เป๊ะๆ หรือเล็กกว่าแล้วมันหมุนได้ตลอด พอใหญ่ขึ้นมาจากนี้แม้แต่นิดเดียวขยับจานคำนวณไม่ได้ซะอย่างนั้น เมื่อการเขียนโปรแกรมครั้งนี้ยังติดปัญหาอยู่หน่อยหนึ่งจึงขอใส่คำว่า “ก้าวแรก” ลงในชื่อบทความ เพื่อจะได้รู้ว่ายังมีส่วนต้องปรับปรุงอยู่อีก คงต้องฝากให้ผู้รู้ท่านอื่นได้ช่วยกันทดลองแก้ไขต่อไป ถ้าได้ผลอย่างไรอย่าลืมกลับมาแลกเปลี่ยนกันบ้างนะครับ ถัดจากนี้ก็เป็นเรื่องของ Source Code กันเต็มๆ ละครับ
Source Code สำหรับการสร้าง Form1
object Form1: TForm1
Left = 345
Top = 126
Caption = 'ตัวอย่างการวาดและหมุนจานคำนวณ 360 องศา'
ClientHeight = 413
ClientWidth = 361
Color = clBtnFace
Constraints.MaxHeight = 750
Constraints.MaxWidth = 676
Constraints.MinHeight = 451
Constraints.MinWidth = 377
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
Position = poDesktopCenter
OnCreate = FormCreate
OnResize = FormResize
PixelsPerInch = 96
TextHeight = 13
object Image1: TImage
Left = 0
Top = 0
Width = 361
Height = 361
Align = alClient
AutoSize = True
IncrementalDisplay = True
OnMouseDown = Form1MouseDown
OnMouseMove = Form1MouseMove
OnMouseUp = Form1MouseUp
ExplicitWidth = 369
end
object Panel1: TPanel
Left = 0
Top = 361
Width = 361
Height = 52
Align = alBottom
TabOrder = 0
DesignSize = (
361
52)
object Label1: TLabel
Left = 10
Top = 23
Width = 228
Height = 13
Caption = 'ค่าดัชนี' = 90 'องศา' 'ขนาดภาพ' 361*361 'สัดส่วน' 1.0'
end
object BitBtn1: TBitBtn
Left = 275
Top = 19
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Kind = bkClose
NumGlyphs = 2
TabOrder = 0
end
end
end
Source Code สำหรับ Unit1
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, Math, StdCtrls, Buttons, ComCtrls, ToolWin;
type
TForm1 = class(TForm)
Panel1: TPanel;
Label1: TLabel;
BitBtn1: TBitBtn;
Image1: TImage;
procedure FormCreate(Sender: TObject);
procedure Form1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure Form1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure Form1MouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure FormResize(Sender: TObject);
private
{ Private declarations }
ChartSize, ChartCenterX, ChartCenterY, X1,X2,Y1,Y2 : Integer;
Index, SubIndex, RotateAngle1, RotateAngle2, Ratio : Real;
DialRotate : Boolean;
Function FindDistance(x,y : Integer): Integer;
Function Normal360(x : Real):Real;
Procedure DrawDial360;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
Function TForm1.Normal360(x : Real):Real;
begin
Repeat
if X<0 then X := X+360;
if X>=360 then X := X-360;
until (X>=0)and(X<360);
Result := X;
end;
Function TForm1.FindDistance(x,y : Integer): Integer;
begin
if X= ChartCenterX then FindDistance := abs(y-ChartCenterY) else if y= ChartCenterY then FindDistance := abs(X-ChartCenterX)
else FindDistance := Trunc(Sqrt(sqr(X-ChartCenterX)+sqr(y-ChartCenterY)));
end;
Procedure TForm1.DrawDial360;
Var I,E : Integer;
begin
With Image1.Canvas do begin
Rectangle(0, 0, Image1.Width, Image1.Height);
Pen.Color := RGB ( 0, 0, 85);
Image1.Canvas.Brush.Style := bsSolid;
Rectangle( 0, 0, Image1.Width, Image1.Height);
Pen.Color := RGB (255, 0,64);
Image1.Canvas.Brush.Style := bsClear;
Ellipse(ChartCenterX-Round(140*ratio), ChartCenterY-Round(140*ratio), ChartCenterX+Round(140*ratio), ChartCenterY+Round(140*ratio));
Pen.Color := ClPurple;
Ellipse(ChartCenterX-Round(120*ratio), ChartCenterY-Round(120*ratio), ChartCenterX+Round(120*ratio), ChartCenterY+Round(120*ratio));
Ellipse(ChartCenterX-Round(10*ratio), ChartCenterY-Round(10*ratio), ChartCenterX+Round(10*ratio), ChartCenterY+Round(10*ratio));
for I:=1 to 12 do begin // 12 Main Lines
SubIndex := Index+(I*30); Normal360(SubIndex);
X1 := ChartCenterX-Trunc(140*ratio*Sin(SubIndex*pi/180));
Y1 := ChartCenterY-Trunc(140*ratio*Cos(SubIndex*pi/180));
X2 := ChartCenterX-Trunc(10*ratio*Sin(SubIndex*pi/180));
Y2 := ChartCenterY-Trunc(10*ratio*Cos(SubIndex*pi/180));
Case I of
3 : Pen.Color := RGB(0,0,255); // First House
12 : Pen.Color := RGB(255,0,0); // Tenth House
else Pen.Color := ClPurple;
end;
Moveto(X1,Y1);
Lineto(X2,Y2);
if I in [3,6,9,12] then begin // Arrow on Square Point
X2 := ChartCenterX-Trunc(120*ratio*Sin((SubIndex+5)*pi/180));
Y2 := ChartCenterY-Trunc(120*ratio*Cos((SubIndex+5)*pi/180));
Moveto(X1,Y1); Lineto(X2,Y2);
X2 := ChartCenterX-Trunc(120*ratio*Sin((SubIndex-5)*pi/180));
Y2 := ChartCenterY-Trunc(120*ratio*Cos((SubIndex-5)*pi/180));
Moveto(X1,Y1); Lineto(X2,Y2);
end;
end;
Pen.Color := RGB(255,0,64);
for I:=0 to 3 do begin // Arrow at 4 of 45 Degree
SubIndex := Index+45+(I*90); Normal360(SubIndex);
X1 := ChartCenterX-Trunc(140*ratio*Sin(SubIndex*pi/180));
Y1 := ChartCenterY-Trunc(140*ratio*Cos(SubIndex*pi/180));
X2 := ChartCenterX-Trunc(120*ratio*Sin(SubIndex*pi/180));
Y2 := ChartCenterY-Trunc(120*ratio*Cos(SubIndex*pi/180));
Moveto(X1,Y1);
Lineto(X2,Y2);
X2 := ChartCenterX-Trunc(120*ratio*Sin((SubIndex+2)*pi/180));
Y2 := ChartCenterY-Trunc(120*ratio*Cos((SubIndex+2)*pi/180));
Moveto(X1,Y1); Lineto(X2,Y2);
X2 := ChartCenterX-Trunc(120*ratio*Sin((SubIndex-2)*pi/180));
Y2 := ChartCenterY-Trunc(120*ratio*Cos((SubIndex-2)*pi/180));
Moveto(X1,Y1); Lineto(X2,Y2);
end;
for I:=1 to 71 do // Every 5 Degree
if I in [6,9,12,18,24,27,30,36,42,45,48,54,60,63,66] then else
begin
SubIndex := Index+(I*5); Normal360(SubIndex);
if (I mod 3)=0 then E:=125 else E:=130;
X1 := ChartCenterX-Trunc(140*ratio*Sin(SubIndex*pi/180));
Y1 := ChartCenterY-Trunc(140*ratio*Cos(SubIndex*pi/180));
X2 := ChartCenterX-Trunc(E*ratio*Sin(SubIndex*pi/180));
Y2 := ChartCenterY-Trunc(E*ratio*Cos(SubIndex*pi/180));
Moveto(X1,Y1);
Lineto(X2,Y2);
end;
for I := 1 to 359 do // Mini Lines Every Degree
if (I mod 5) = 0 then else
begin
SubIndex := Index+I; Normal360(SubIndex);
X1 := ChartCenterX-Trunc(140*ratio*Sin(SubIndex*pi/180));
Y1 := ChartCenterY-Trunc(140*ratio*Cos(SubIndex*pi/180));
X2 := ChartCenterX-Trunc(135*ratio*Sin(SubIndex*pi/180));
Y2 := ChartCenterY-Trunc(135*ratio*Cos(SubIndex*pi/180));
Moveto(X1,Y1);
Lineto(X2,Y2);
end;
end; // With
Label1.Caption := 'ค่าดัชนี = '+FloatToStr(index)+' องศา'+' ขนาดภาพ '+InttoStr(Image1.Width)+'*'+InttoStr(Image1.Height)+' สัดส่วน '+FloatToStr(Ratio);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Ratio := 1;
ChartSize := Image1.Width;
ChartCenterX := Round(Image1.Width/2);
ChartCenterY := Round(Image1.Height/2);
Index := 90;
DrawDial360;
end;
procedure TForm1.Form1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
Var MinVal, MaxVal : Longint;
begin
MinVal := Trunc(10*ratio);
MaxVal := Trunc(140*ratio);
if FindDistance(x,y) in [MinVal..MaxVal] then begin
DialRotate := True;
RotateAngle1 := ArcTan2((X-ChartCenterX),(y-ChartCenterY))*180/pi;
Screen.Cursor := crHandPoint;
Image1.Cursor := Screen.Cursor;
end;
end;
procedure TForm1.Form1MouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
Var MinVal, MaxVal : LongInt;
begin
MinVal := Trunc(10*ratio);
MaxVal := Trunc(140*ratio);
if DialRotate and (FindDistance(x,y) in [MinVal..MaxVal]) then begin
RotateAngle2 := ArcTan2((X-ChartCenterX),(y-ChartCenterY))*180/pi;
Screen.Cursor := crHandPoint;
Image1.Cursor := Screen.Cursor;
Index := Index+(RotateAngle2-RotateAngle1);
Index := Normal360(Index);
DrawDial360;
RotateAngle1 := RotateAngle2;
end;
end;
procedure TForm1.Form1MouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
If DialRotate then begin
DialRotate := False;
Screen.Cursor := crDefault;
Form1.Cursor := Screen.Cursor;
end;
end;
procedure TForm1.FormResize(Sender: TObject);
begin
Image1.Width := Form1.Width-16; Image1.Picture.Graphic.Width := Image1.Width;
Image1.Height := Form1.Height-90; Image1.Picture.Graphic.Height := Image1.Height;
If Image1.Width < Image1.Height then ChartSize := Image1.Width Else ChartSize := Image1.Height;
Ratio := ChartSize/361;
ChartCenterX := Round(Image1.Width/2);
ChartCenterY := Round(Image1.Height/2);
DrawDial360;
// ห้าม Image1 เล็กกว่าค่าเริ่มต้น 361*361
// และจะเริ่มหมุนจานไม่ได้เมื่อ Image1 มีขนาดเกิน 660*660
// จึงต้องกำหนดค่า Constraints ของ Form1 เอาไว้ดังนี้
// MaxHeight 750 MaxWidth 676 Min Height 451 MinWidth 377
end;
end.