Lấy nhanh danh sách các Item đã được chọn trong Lisbox

Liên hệ QC

hoa35ktxd

Thành viên thường trực
Tham gia
8/2/07
Bài viết
298
Được thích
269
Chắc hẳn các bạn đã đôi lần làm việc với Listbox. Bình thường với 1 LB có vài trăm dòng thì vấn đề không quan trọng.
Bây giờ ta có 1 LB với khoảng vài chục ngàn dòng, làm thế nào để lấy nhanh ra danh sách các Item đã được chọn, không dùng For Next để duyệt các Selected Item vì sẽ ảnh hưởng khá nhiều đến tốc độ, không dùng công cụ của .Net
Tôi đã nghĩ ra cách để xử lý 2 trường hợp:
- Trường hợp 1: Các Item được chọn bằng việc Click và Ctrl+Click thì mỗi lần chọn ta lưu giá trị ListIndex vào 1 mảng
- Trường hợp 2: Chọn các Item bằng việc bấm và quét chuột qua các Item (mảng liên tục)
Còn lại các trường hợp khác thì chưa giải quyết được như chọn Item bằng cả 2 cách trên hoặc Click then Shift+Click
Bạn nào có cách nào hay chỉ cho tôi với.
Xin cảm ơn nhiều.
 
Lập trình thì tới bước cuối cùng mới sử dụng tới tip & trick. Cái gì cũng có cách giải quyết theo phương pháp chuẩn của nó.

Như tôi nói ở trên, bài toán của bạn chắc chắn là bài toán chuẩn rồi (vì kiểu gì đám Microsoft coders trước kia cũng phải đụng tới).

Và nó được giải quyết thế này:

Conventional Visual Basic wisdom states that in order to gather
the selected items from a multi-select ListBox, you should loop
through all the items and test the Selected property. As with
all loops, however, this can potentially bog down slower CPU's.
As a much faster and more elegant alternative, you can use the
SendMessage() API function instead.

As you probably know, this function lets you send a message to
one or more windows. The declaration statement conforms to the
following syntax:

Mã:
Private Declare Function SendMessage Lib "user32" _
   Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg _
   As Long, ByVal wParam As Long, lParam As Any) As Long

Since we want to gather the listbox's selected items, we'll
send the LB_GETSELITEMS constant in the wMsg argument, which
you declare like so:

Mã:
Private Const LB_GETSELITEMS = &H191

In essence, the LB_GETSELITEMS message fills an array with the
index numbers of all the selected items. As a result, you must
pass two additional arguments with SendMessage(). The first
argument should contain the maximum number of selected items.
To retrieve this value, you can simply use the listbox's
SelCount property. The second argument should hold the array
variable you want to fill with index values. The following
example shows how you might use this function:

Mã:
Dim ItemIndexes() As Long, x As Integer, iNumItems As Integer
iNumItems = ThisBox.SelCount
If iNumItems Then
   ReDim ItemIndexes(iNumItems - 1)
   SendMessage ListBox1.hwnd, LB_GETSELITEMS, iNumItems, _
     ItemIndexes(0)
End If
For x = 0 To iNumItems - 1
   MsgBox ListBox1.List(ItemIndexes(x))
Next x

After being passed to the SendMessage function, iNumItems holds
the total number of selected items, and the ItemIndexes array
holds the selected item index values. Notice, that you must
pass a pointer to the ItemIndexes array, and not the array
itself. Thus, we passed ItemIndexes(0) into the SendMessage
function, not ItemIndexes().

99% các câu hỏi liên quan tới lập trình (xử lý kỹ thuật) đều có thể giải quyết theo bước:
- Dịch câu hỏi sang tiếng anh
- Sử dụng Google

Ngoài "câu chuyện" của bạn là: gather/get/remove multi-selected VB listbox items như đã nói ở trên, bạn còn phải quan tâm tới limits của standard listbox (vì thế ít khi PM xịn sử dụng listbox cho dữ liệu nhiều). Nếu ko bạn sẽ phải xử lý chuyện > 64k dữ liệu được load lên standard listbox thế nào? Làm thế nào để trong nháy mắt có thể load bụp 1 cái 1tr bản ghi lên listbox :P, v.v...

Ngoài ra, bạn còn phải nghĩ tới việc duplicate copy from a listbox to another, remove selected listbox items, etc... (all w/ fastest way). Tất cả những việc như vậy đang đợi bạn và tôi chỉ có thể gợi ý đến đây mà thôi.
 
Lần chỉnh sửa cuối:
Upvote 0
Cảm ơn bài viết của bạn, vì tôi không phải dân IT nên không nghiên cứu sâu và cụ thể về các vấn đề này, thấy cái gì có sẵn thì dùng theo kiểu nông dân thôi.
Để nhiên cứu thử cái API của bạn xem sao. Tuy nhiên có một số chỗ tôi thấy không hiểu
- iNumItems = ThisBox.SelCount' Cái này lấy ở đâu
- ListBox1.hwnd' cái này trong VBA làm sao mà lấy được hwnd
Hình như bạn hướng dẫn cho VB6 thì phải.
 
Upvote 0
Như bác Hải nói, Microsoft đã chấp nhận thì khó có cách tối ưu hơn.Chấp nhận các cách đó thôi, may là chương trình của mình chỉ hơn chục đối tượng trong listbox.
 
Upvote 0
List1.SelCount --> Cái này là thuộc tính của Listbox mà. (Tên Control thì bạn có thể thay: ThisBox --> Listbox1 gì đó)

Nếu control ko có handle thì bạn có thể lên mạng, google cách lấy Hwnd của windowless control. Chỉ 1 cái google là bạn có thể làm được việc đó mà (Chỉ khoảng 1 vài dòng thôi). Ngay cả trong VB6, Label ko có Hwnd nhưng ta cũng có thể Get Handle được (và vì thế mới có thể show balloon tooltip cho label).

Còn nếu ko muốn sử dụng APIs thì đúng là chỉ có cách dùng mẹo mà thôi (nói chung là ko nên dùng mẹo vì cái gì cũng có giới hạn/mặt trái của nó. Bạn mà dùng mẹo thì code của bạn phải thuộc dạng well comments, nếu ko chả ai có thể đọc hiểu được). Thêm nữa, mẹo thì thường phải tự mình cover tất cả các trường hợp (như câu hỏi của bạn đó)
 
Lần chỉnh sửa cuối:
Upvote 0
Một lần nữa xin cảm ơn bạn.
Tuy nhiên, vấn đề của tôi cần giải quyết nằm trong VBA chứ không phải VB6 hay .Net
Ở đây thật đáng tiếc vì VBA không có cái thuộc tính SelCount, VB6 thì có cái này, .Net còn hiện đại hơn có luôn SelectedItems. Có lẽ đành phải làm thủ công vậy.
Cảm ơn các bạn nhiều.
 
Upvote 0
Nếu đã biết M$ cố tình ko làm những thứ đó trong VBA thì care làm gì nữa. Cứ chân phương mà làm. Nếu điều đó là quá cần thiết thì học cách sử dụng DLL từ Excel VBA là xong.
 
Upvote 0
Trời, tôi muốn tìm được cách làm đơn giản trực tiếp trong VBA thôi, không đao to búa lớn làm gì vì cồng kềnh phức tạp.
Còn muốn xử lý như bạn nói thì tôi dùng .Net cho đơn giản hơn nhiều, chả phải DLL gì cho phức tạp và tốn công.
Thân.
 
Upvote 0
Trời, tôi muốn tìm được cách làm đơn giản trực tiếp trong VBA thôi, không đao to búa lớn làm gì vì cồng kềnh phức tạp.
Còn muốn xử lý như bạn nói thì tôi dùng .Net cho đơn giản hơn nhiều, chả phải DLL gì cho phức tạp và tốn công.
Thân.

Vấn đề là con người khi chọn đường đi thì cần phải biết đường đó có đi được hay ko. Khi đã chứng minh là có tảng đá to lù lù trước mặt rồi thì nếu mà còn cố đi tiếp người ta gọi là....

Chúng ta phải biết cách học giải quyết vấn đề (nếu tôi làm VBA thật thì chắc chỉ khoảng 10 phút là tôi biết cái đó có làm được trên VBA hay ko. VBA hay Excel là đồ của M$ làm ra, chỉ cần vào msdn seach 1 lúc ko thấy đáp ứng thì về mặt chính thức là ko có cách nào xử lý. Mà khi M$ tạo ra nó nói đó là limited thì tốt nhất đừng tiếp tục làm tiếp vấn đề đó vì cái đó gọi là "biết là ko được mà cứ làm").

Trong quá trình giải quyết vấn đề, khi làm ko được thì tìm, tìm không được thì làm cách khác chứ ko đi tiếp vào ngõ cụt (google & microsoft mà "solution not found" thì tớ cam đoan là chả có ai trên GPE làm được).

Bạn khi làm việc với Excel, với VBA thì phải hiểu là cái đó rất hạn chế. Properties và Methods của controls hạn chế như vậy thì tìm tiếp làm gì. Thế nên người ta mới gọi là VBA. Excel chỉ mạnh ở cái bảng tính với dữ liệu nhỏ thôi (mở files 500,000 dòng thì biết ngay) và VBA chỉ là 1 phần nhỏ script language bổ sung vào mà thôi.
 
Lần chỉnh sửa cuối:
Upvote 0
Ở đây thật đáng tiếc vì VBA không có cái thuộc tính SelCount

Để lấy được SelCount của ListBox thì bạn làm như sau:

Đoạn này để ở module nào đó
Mã:
Public Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
Public Const LB_GETSELCOUNT = &H190

Mã:
Public Function fnListBox_SelCount(objListBox as ListBox) as Long

    ' get the number of selected items in the listbox
    fnListBox_SelCount = SendMessage(objListBox.hWnd, LB_GETSELCOUNT, 0&, ByVal 0&)

End Function

Sử dụng:

...
'// iNumItems = Listbox1.SelCount
iNumItems = fnListBox_SelCount(Listbox1)
....

Tớ vừa tra trên API Declaration và thấy hằng số LB_GETSELCOUNT nên đoán là nó sẽ chạy vì nếu viết như thế này thì nó sẽ lấy được tương tự như thuộc tính .ListCount

Mã:
    ' get the number of items in the source list
    numItems = SendMessage(objListBox.hWnd, LB_GETCOUNT, 0&, ByVal 0&)

Nếu đoạn code trên mà thực hiện được thì coi như bài toán đã được giải quyết.
(Trước đó phải lấy được handle của objListBox đã vì trong VBA thì Listbox là Windowless control. Sử dụng hàm FindWindowEx)

Về mặt lý thuyết mà nói thì mọi ActiveX Controls trong HĐH Windows đều là các Window và đều có thể mở rộng nó thành những Window chuẩn. VBA chẳng qua cũng dùng tương tự như VB6 nhưng nó làm hẹp phạm vi đi mà thôi. Còn những gì trước kia phải làm bằng APIs với VS6 thì nay ông M$ biến nó thành bộ .NET Framework với hàng trăm nghìn tính năng được tích hợp theo từng object và trở thành thư viện để cho chúng ta dùng 1 cách dễ dàng.
 
Lần chỉnh sửa cuối:
Upvote 0
Cách lấy handle của ListBox:

hWnd = FindWindowEx(Me.hwnd, 0, "ThunderListBox", vbNullString)

Thậm chí có thể tạo ListBox at runtime như

Mã:
...
    CName = "LISTBOX" 'ListBox
    CreateListBox
...

Private Sub CreateListBox()
h = CreateWindowEx(WS_EX_CLIENTEDGE, CName, "", WS_BORDER Or WS_VSCROLL Or WS_CHILD Or WS_VISIBLE, 170, 20, 160, 120, [B]Form1.hwnd[/B], vbNull, App.hInstance, ByVal 0&)
AddItemsToList h, LB_ADDSTRING
End Sub

Còn nếu form mà ko có hwnd thì .... thôi, cancel vụ này đi. Làm tiếp chỉ tổ tốn cơm. Còn nếu làm tiếp dạng tips & trick thì đó ko phải là pro code và rất dễ dẫn tới việc ko handle hết tất cả các trường hợp.

Như vậy, M$ đã muốn vậy trong VBA rồi thì chúng ta còn take care làm gì nữa

:D
 
Lần chỉnh sửa cuối:
Upvote 0
Cảm ơn bạn hai2hai đã dành nhiều thời gian cho vấn đề của tôi.
Sau 1 hồi mày mò, tôi cũng đã làm được cái tôi cần, không dùng API, chỉ thông qua các thao tác khi sử dụng để lưu lại những Item được chọn. Có vẻ hơi dài dòng nhưng tôi thấy hiệu quả.
Các bạn tham khảo và cho ý kiến nhé, có thể vẫn còn sót tình huống.
PHP:
Dim Str As String 'Chuỗi lấy danh sách các Item được chọn
Dim Mdn As Boolean 'MouseDown
Dim Mm As Boolean ' MouseMove
Dim ShiftK As Integer
Dim StID As Long 'Vị trí ban đầu
Dim BotID As Long, TopID As Long 'Vị trí đầu, cuối danh sách
Private Sub L1_Change()
    If ShiftK = 0 Then 'Không có phím điều khiển nào được nhấn
        If Not Mm Then 'Không kéo rê chuột
            StID = L1.ListIndex
            Str = "(" & L1.ListIndex & ")"
        Else 'Kéo rê chuột
            LienTuc
        End If
    ElseIf ShiftK = 1 Or ShiftK = 3 Then 'Bâìm Shift hoãòc Ctrl+Shift
        LienTuc
    ElseIf ShiftK = 2 Then 'Bấm Ctrl
        If Not Mm Then 'Không kéo rê chuột
            If L1.Selected(L1.ListIndex) Then StID = L1.ListIndex
            If InStr(Str, "(" & L1.ListIndex & ")") <> 0 Then
                Str = Replace(Str, "(" & L1.ListIndex & ")", "")
            Else
                If Mdn Then 'Chọn bằng chuột
                    Str = Str & "(" & L1.ListIndex & ")"
                Else 'Chọn bằng phím
                    Str = "(" & L1.ListIndex & ")"
                End If
            End If
        Else 'Kéo rê chuột
            LienTuc
        End If
    End If
    Me.Caption = Str
End Sub
Private Sub LienTuc()
    Str = ""
    If StID > L1.ListIndex Then
        BotID = StID: TopID = L1.ListIndex
    Else
        BotID = L1.ListIndex: TopID = StID
    End If
    For I = TopID To BotID
        Str = Str & "(" & I & ")"
    Next
End Sub
Private Sub L1_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    ShiftK = Shift
End Sub
Private Sub L1_KeyUp(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    ShiftK = Shift
End Sub
Private Sub L1_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
    ShiftK = Shift
    Mdn = True
End Sub
Private Sub L1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
    ShiftK = Shift
    If Mdn Then Mm = True
End Sub
Private Sub L1_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
    ShiftK = Shift
    Mm = False: Mdn = False
End Sub
Private Sub UserForm_Activate()
    Dim Lst() As Long
    For I = 1 To 1000
        L1.AddItem I
    Next
End Sub
 
Lần chỉnh sửa cuối:
Upvote 0
Web KT

Bài viết mới nhất

Back
Top Bottom