วันอาทิตย์ที่ 13 กุมภาพันธ์ พ.ศ. 2554

6: รายงานเป็นหน้าต่างของผลงาน

รายงานเป็นหน้าต่างของผลงาน

วัตถุประสงค์

นำเสนอโครงสร้างของรายงาน รู้จัก EVENT ของรายงาน และการเรียกใช้รายงานด้วย VBA

เนื้อหา

                ส่วนของ Report ใน Access เป็นองค์ประกอบที่ทำให้งานที่ผมทำออกมา ดูมีราคาขึ้นมากเลยครับ เพราะ Access เอื้อให้เราสร้างรายงานได้ง่าย สวยงาม  ซึ่งหากมาวิเคราะห์กันจริงๆ  ก็มีเครื่องมือในการออกแบบรายงานแบบนี้มานานแล้ว  ตั้งแต่ Access Version 2  โดยทั่วไปการทำรายงานใน Access จะคล้ายกับ Crystal report  แต่ความยืดหยุ่นจะสูงกว่า
                เชื่อว่ากว่าจะมาถึงตรงนี้ หลายๆ ท่านคงได้เคยใช้ Access ในการออกรายงานมาบ้างแล้ว  บทนี้จะมีส่วนที่เกี่ยวกับการเขียนโปรแกรมไม่มาก แต่ผมอยากเล่าประสพการณ์ และ tip ในการออกรายงานด้วย Access เป็นหลักครับ
หาข้อมูลมากออกรายงานให้ได้
                ก่อนที่จะทำการทำรายงานสักรายงาน เราต้องทราบให้ได้ว่าข้อมูลที่จะมาแสดงในรายงานของเรา จะมีลักษณะ Record เป็นอย่างไร ใช้ Query โดยการหยิบ Field จาก Table ใดบ้าง  เราจะกำหนดไว้ใน Record Source ของรายงาน
สมมุติโจทย์ว่าเราต้องการรายงานที่แสดงยอดขายในแต่ละเดือน และ ปี แยกรายการสินค้า โดยใช้ Table ของ Northwind นะครับ   รายการขายของ Northwind จะอยู่ใน Table Orders และ Order Detail
รายงานและข้อมูลดิบที่ต้องการนำมาแสดง

                สิ่งที่ยากก็คือเราต้องแยกให้ออกว่า ข้อมูลส่วนใดคือข้อมูลดิบจริง  มีหลักง่ายๆ ว่า
·        รายการดิบที่จะเป็นต้นทางนั้นจะเป็นรายการต่อ 1 บรรทัดในรายงาน ที่เรียกว่า Detail   เราต้องดูที่ Detail เป็นหลักเลยครับ ว่าจะมี Field อะไรบ้าง  เช่นกรณีนี้ต้องการให้มี Product id, Product name  และ รวมยอดขาย   
·        การสรุปรวม ต่ออะไรบ้าง  การสรุปรวมแยกแต่ละส่วนในรายงานปลายทาง ก็จะเป็น Field ที่เราต้องหยิบมาเป็นข้อมูลดิบในการออกรายงานด้วย  กรณีนี้เรากำลังสรุปแยก ปี และ เดือน ด้วย  ปีและเดือนจึงต้องกลายเป็นส่วนของ record
·        ในส่วนที่เป็น Detail ในบางครั้งรายการที่แสดง ไม่ใช้เป็นรายการดิบๆ แต่มีการรวมตัวเลข  นั้นหมายความว่าช่องตัวเลขนั้น  ต้องมาจากการใช้ Query ที่เป็น Group By ช่องใดที่เกิดจากการรวมตัวเลข เราก็จะใช้ Sum ที่ Field นั้นๆ  กรณีนี้ รายการย่อยๆ มีการ Sum ที่ ยอดขาย  เราจึงทำQuery เป็น Group by แล้ว Sum ที่ Field นี้
ให้ลองสร้างรายงานใหม่ขึ้นมา 1 รายงาน แล้วกำหนด Query ที่ใช้ในรายงานดูนะครับ
SELECT     Year([ShippedDate]) AS YearGroup,
  Month([ShippedDate]) AS MonthGroup,
  Sum([Order Details].Quantity) AS SubQuantity,
  Products.ProductID,
  Products.ProductName
FROM        Products INNER JOIN
          (Orders INNER JOIN [Order Details] ON Orders.OrderID = [Order Details].OrderID)  ON Products.ProductID = [Order Details].ProductID
WHERE      (((Orders.ShippedDate) Is Not Null))
GROUP BY   Year([ShippedDate]), Month([ShippedDate]), Products.ProductID,
  Products.ProductName;
รวมตัวเลขตาม Group
การสรุปรวมตัวเลขในแต่ละปี หรือ เดือน   ก็เพียงแต่สั่งให้ Access สร้าง บริเวณ Group Header / Footer เพิ่มขึ้นมาครับ   เราก็จะนำ Field ไปวาง  แต่เราจะใส่ Function แทนการใส่ Field ธรรมดา เช่นใส่ว่า =Sum([SubQuantity])  ตาม ตยนี้เป็นต้น ( subQuantity เป็นชื่อ Field ที่ผม Sum Quantity มาจาก Query ไว้แล้ว )

รู้จัก REPORT EVENT
ภายในตัว Report มี Event ที่เราสามารถนำมาทำงานอะไรได้เยอะครับ  แต่เราเข้าไปเกี่ยวข้องกับการเขียน code บน Report น้อยกว่า Form มาก ด้วยหลายๆ เหตุผลครับ  หลักๆ คือรายงานควรทำง่ายแก้ง่าย หากผูก code ไว้มากมาก จะทำให้ดูแลโปรแกรมได้ยาก  
อย่างไรก็ดี เราควรรู้จักกับ Event ของ Report ที่สามารถช่วยให้เราสามารถ ออก Report ที่ซับซ้อนได้มากขึ้น  เรามีแต่ Event บนตัว Report เท่านั้นนะครับ  ไม่มี Event บน Control ที่อยู่บน Report เนื่องจาก เราไม่มีการ บันทึกข้อมูลใน Report จึงไม่มี Event ของ Control บน Report เหมือนกับ Form 
เราแบ่ง Event ได้เป็น กลุ่มใหญ่ ๆ คือ
·        Event ของตัว Report
·        Event เมื่อมีการประมวผล หรือพิมพ์รายการในส่วน Detail
·        Event เมื่อมีการทำ Page Header/Footer เมื่อมีการกำหนดให้มี Page Header /Footer
·        Event เมื่อมีการทำ Report Header/Footer  เมื่อมีการกำหนดให้มี Report Header /Footer
·        Event เมื่อมีการทำ Group Header / Footer  สำหรับกรณี Report ที่มี Group Header / Footer โดยแยกเป็น Event ของ Group แต่ละตัว
Event ของ Report
คล้ายกับชุด Event ของ Form มี Open, Close, Activate, Deactivate, Error  เช่นกัน มีกลไกการทำงานแบบเดียวกับ Form ทุกประการ มี Event ที่แตกต่างคือ
§    NODATA EVENT
Event ตัวนี้เกิดขึ้นเมื่อ Report ตัวนั้น ไม่มีข้อมูลที่จะนำมาแสดง หรือ RecordSource ที่กำหนดไว้ ให้จำนวน Record เป็น 0  เราสามารถ Cancel การ Open Report ตัวนี้ได้โดยการระบุ Cancel = True , Event ตัวนี้มีประโยชน์มาก เพราะ ช่วยให้การพิมพ์ Report ที่ไม่มีข้อมูล ไม่ให้แสดงข้อความว่า Error บนหน้ากระดาษ โดยการยกเลิก Report เสียก่อน

Private Sub Report_NoData(Cancel As Integer)
       MsgBox ("ไม่พบข้อมูลในรายงาน")
       Cancel = True
End Sub
Event ของ Detail
ทุกทุก บรรทัดของข้อมูลที่มีการแสดงบน Detail มี Event กลุ่มนี้เกิดขึ้นเสมอ เราสามารถทำการพิเศษๆ บน Event เหล่านี้ได้  ส่วนใหญ่เราใช้ Event Format ส่วน อีก 2 ตัวเราใช้น้อยดังต่อไปนี้
§   FORMAT EVENT
เกิดขึ้นเมื่อ Access ดึงข้อมูลมาวางบน Report และเริ่มจัดรูปแบบเพื่อเตรียมพิมพ์  ใน EVENT นี้เราสามารถอ่านค่าของ Control มาทำการคำนวณพิเศษๆ ได้ ในทุกๆ บรรทัดของข้อมูลที่มีการอ่านมาแสดงผล
สมมุติว่าเราไม่ใส่ pages ( จำนวนหน้า ) ลงใน Control , Access จะไม่อ่านข้อมูลจนหมด เพื่อทดสอบว่า รายงานของเราต้องใช้กี่หน้า แต่ถ้าเรากำหนด ก็จะเกิด Event นี้เท่ากับจำนวน Record ที่มีทันทีเพื่อ Access จะได้ทราบว่าต้องใช้กระดาษกี่หน้า   และหากมีการสั่งพิมพ์อีกครั้งหนึ่ง Event เหล่านี้จะเกิดซ้ำ  ดังนั้นการทำการบน Event จะต้องคำนึงถึงการกลับมาเรียกซ้ำได้  ของ Event นี้ 
นอกจากนี้ การตั้งให้ Object CanGrow, CanShrink ก็เป็นส่วนที่ทำให้ มีการทำการ Format ซ้ำได้ เพราะ Access ต้องทดสอบจำนวนหน้าโดยการวาง Control ลงบนหน้ากระดาษ หากล้นหน้า ก็ต้องขึ้นหน้าใหม่ แล้วทดลองวาง Control ใหม่  ตรงนี้จะเกิด Event ซ้าซ้อน ซึ่งต่างจาก Case แรก เพราะ Event ที่ซ้ำจะเกิดติดๆกันในระหว่าง 1 Control   เราสามารถตรวจสอบการเรียกซ้ำ จากค่า FormatCount ถ้า FormatCount > 1 แสดงว่าเป็นการเกิด Event Format ซ้ำซ้อน ไม่ได้เกิดในครั้งแรก เป็นต้น
ในแต่ละ Detail เราสามารถสั่งให้มีการยกเลิกรายการบน Report ได้โดยใช้คำสั่ง Cancel = True เช่นกัน วิธีนี้ทำให้รายการที่ถูก Cancel หายไปจาก รายงาน  ตัวอย่าง Code ชุดนี้เป็นการบังคับให้แสดงเฉพาะรายชื่อ Supplier ที่มีรหัสเป็นเลขคู่ ทดลองสร้าง รายงานที่มี Supplier เป็น Recordsource  และบรรจุ Code ชุดนี้เข้าไป

Private Sub Detail_Format(Cancel As Integer, FormatCount As Integer)
        If (Me.SupplierID And 1) > 0 Then
           Cancel = True
        End If
End Sub
§   RETREAT EVENT
Event นี้ไม่เหมือนกับ Format เป็น Event ที่อาจเกิดมากกว่า 1 ครั้งได้เหมือนกัน Event นี้คล้ายกับการทำ Format ซ้ำ แต่เกิดขึ้นระหว่าง Section ของ Report เช่นระหว่างที่ทำการ Format Detail อยู่ แล้วต้องขึ้นหน้าใหม่ เพราะหน้ากระดาษไม่พอ  แต่มีการกำหนดให้ Group Header ขึ้นทุกหน้า  จึงเกิดการ Format ส่วน Group header ตรงนี้จะเกิด Event Retreat บน GroupHeader แล้วเมื่อทำการ Format เสร็จ ก็จะกลับมาที่ Detail อีกครั้ง แล้วเริ่มทำการพิมพ์ข้อมูลในส่วน Detail ต่อไป  ตรงนี้จะเกิด Event Retreat บน Detail อีกครั้งหนึ่ง ในขณะกลับมาจาก GroupHeader,  Event นี้คล้ายกับการ GotFocus ของ Form นั่นเอง  ไม่ค่อยได้มีโอกาสได้ใช้เท่าไร
§   PRINT EVENT
เมื่อทำการพิมพ์ข้อมูลลงบน Report  เกิดหลังจากทำการ Format เสร็จสมบูรณ์แล้ว ดังนั้นเราจะตรวจสอบที่ Format ก่อน แล้วหากเราไม่ทำการ Cancel , ก็จะเกิด Event นี้ขึ้น เราอาจทำการตรวจสอบอีกครั้งที่ Event นี้ก็ได้    แต่ในทางปฏิบัติ ก็ใช้ Format Event แทนกันได้  ใน Event นี้ก็มี Format Count ให้ตรวจสอบการเรียก Event นี้ซ้ำเช่นกัน และสามารถ Cancel ได้เหมือน Format ทุกประการ
EVENT ของ Page / Report / Group
เหมือนกับ Event ของ Detail แต่จังหวะในการเกิด Event ไม่เหมือนกัน แตกต่างกันไปตามประเภทของ Group by ที่เราสร้างขึ้นมา  แต่ทุกตัวก็จะแบ่งได้เป็น 2 ชุดหลักๆ คือ เป็น Header กับ Footer   

EVENT
คำอธิบาย
ReportHeader_Format
เมื่อมีการเริ่มทำการ Format หัว Report เกิดขึ้นหนึ่งครั้งเท่านั้น เรามักใช้ในการ Initial ค่าตัวแปรเริ่มต้น หากต้องมีการทำการคำนวณพิเศษๆ ใต้ Detial
ReportFooter_Format
เมื่อมีการเริ่มทำการ Format ท้าย Report  ใช้สำหรับการคำนวณปิดท้าย
PageHeader_Format
เมื่อมีการเริ่มทำการ Format หัวกระดาษ 
PageFooter_Format
เมื่อมีการเริ่มทำการ Format ท้ายกระดาษ
GroupnHeader_Format
เมื่อมีการเริ่มทำการ Format Group ที่ n ที่หัวกระดาษ  สำหรับ Set ค่าตัวแปรในแต่ละหมวด
GroupnFooter_Format
เมื่อมีการเริ่มทำการ Format Group ที่ n ที่ท้ายกระดาษ ใช้สำหรับการคำนวณปิดท้ายของแต่ละ Group


Record Source สำคัญมากมาก
เพราะ Record Source ที่เรานำมาใช้แสดงรายงาน เป็นฐานข้อมูลที่จะปรากฎในรายงาน ความช้าเร็ว ของรายงาน ส่วนหนึ่งก็ขึ้นกับว่าเราทำ Record Source ที่มาแสดงในรายงานไว้ดีหรือไม่   เราควร Join table อย่างไร จึงทำให้รายงานเร็ว ? อันต้องขึ้นกับการออบแบบฐานข้อมูลไว้ในตอนแรก ประกอบกับ การเข้าความสัมพันธ์ของ Table ว่า Link กันอย่างไร  หาก link ผิดไม่เป็นตามโครงสร้าง ก็จะช้ามากมาก
เปลี่ยน Record source ของ Report
ทำได้ไม่ยาก  ปกติเราจะเขียนโปรแกรมไว้ที่  Event ของ Report_Open เพื่อใช้เปลี่ยน Record Source  ลองแก้ไข ตย ที่ผ่านมาครับ แล้วใส่ code ชุดนี้ครับ  สามารถเลือกปีได้ครับ เพื่อนำไป where ข้อมูลที่จะออกรายงาน
Option Compare Database
Private Sub Report_Open(Cancel As Integer)
        Dim x
        Dim sqlx
        x = InputBox("Enter year, blank for all")
        If x = "" Then
           sqlx = " SELECT  Year([ShippedDate]) AS YearGroup, " & _
                  "         Month([ShippedDate]) AS MonthGroup, " & _
                  "         Sum([Order Details].Quantity) AS SubQuantity, " & _
                  "         Products.ProductID, " & _
                  "         Products.ProductName " & _
                  " FROM    Products INNER JOIN " & _
                  " (Orders INNER JOIN [Order Details] ON Orders.OrderID = [Order Details].OrderID) “ & _
                  “ ON Products.ProductID = [Order Details].ProductID " & _
                  " WHERE(((Orders.ShippedDate) Is Not Null)) " & _
                  " GROUP BY    Year([ShippedDate]), Month([ShippedDate]), Products.ProductID, " & _
                  " Products.ProductName; "
        Else
           sqlx = " SELECT  Year([ShippedDate]) AS YearGroup, " & _
                  "         Month([ShippedDate]) AS MonthGroup, " & _
                  "         Sum([Order Details].Quantity) AS SubQuantity, " & _
                  "         Products.ProductID, " & _
                  "         Products.ProductName " & _
                  " FROM    Products INNER JOIN " & _
                  " (Orders INNER JOIN [Order Details] ON Orders.OrderID = [Order Details].OrderID) “ & _
                  “ ON Products.ProductID = [Order Details].ProductID " & _
                  " WHERE(((Orders.ShippedDate) Is Not Null)) and Year([ShippedDate])= " & x & _
                  " GROUP BY    Year([ShippedDate]), Month([ShippedDate]), Products.ProductID, " & _
                  " Products.ProductName; "
        End If
        Me.RecordSource = sqlx
       
End Sub

การเรียกรายงานด้วย VBA
                คำสั่งง่ายๆ ครับในการเปิดรายงานคือ docmd.openreport  ตามด้วยชื่อ Report ที่เป็น String  เป็นคำสั่งธรรมดาครับ   แต่ที่น่าสนใจคือ เราสั่งให้รายงานเปิดแบบมีเงื่อนไขได้ โดยการระบุที่ where 
ผมจะทดลองทำ form มาเปิดรายงานชื่อ “Summary of Sales by Year” ซึ่งเป้นรายงานใน Northwind.mdb นะครับ  ให้เลือกปีได้
1.       สร้าง Form เปล่าๆ 1 Form
2.       วาง Control ที่เป็น text
box 2
ตัว ตั้งชื่อว่า txtYearFrom กับ txtYearTo
3.       สร้างปุ่ม สำหรับสั่งพิมพ์
4.       วาง checkbox สำหรับ ให้เลือก Preview หรือ Print เลย ให้ชื่อว่า chkPrint
5.       ใส่ Code ตามนี้เลยครับ
แสดงปุ่มสำหรับเปิดรายงาน และ code


                จากโปรแกรมจะเห็นว่า เรามีการสร้าง textbox สำหรับใส่ ปีที่จะพิมพ์ แล้วเราก็นำปีที่ได้มาสร้างเป็น ตัวแปลในขณะจะทำการพิมพ์รายงาน โดยการทำเป็นเงื่อนไขไว้ใน ตัวแปร WX แล้วส่งให้ docmd.openreport   ไปเปิดต่ออีกทีครับ ส่วน "YEAR(ShippedDate) between " & Me.txtFromYear & " and " & Me.txtToYear เป็นการสร้าง เงื่อนไขว่า ปีของ Shippeddate ต้องอยู่ในช่วง ของค่าปีที่ใส่ใน textbox ชื่อ txtYearfrom กับ txtYearto
                ค่า previewX เป็นตัวแปรที่เก็บว่า  ตอนนี้ user เลือกที่จะ Preview หรือ จะพิมพ์ ตรงๆออกจากเครื่องพิมพ์  ค่า acViewPreview กับ acViewNormal เป็น ค่าคงที่เรียกใช้ได้จาก Access ครับ

ลองทำให้เลือกรายงานได้
                ชื่อรายงานที่สั่งพิมพ์ ในตย ข้างต้นเป็น Summary of Sales by year ลองทำเป็นตัวแปรดูครับ แทนที่จะเป็นชื่อคงที่ ก็ให้ user เลือกชื่อรายงานแทน โดยการทำเป็น textbox ครับ ให้ User มาใว่ชื่อรายงานที่จะพิมพ์  ถ้าจะหรูกว่านี้ ก็ต้องสร้างตารางมาเก็บชื่อรายงานที่เข้าใจง่ายๆ กับ  ชื่อรายงานใน Access เพื่อให้ user เรียกง่าย  และแทนที่จะเป็น Textbox ก็เป็น combobox แทน  ลองทำดูนะครับ
               

ไม่มีความคิดเห็น:

แสดงความคิดเห็น