Chuyên đề giải đáp những thắc mắc về code VBA

Liên hệ QC

maytinhvp01

Thành viên thường trực
Tham gia
27/7/13
Bài viết
390
Được thích
179
Mình muốn nhờ giải thich câu lệnh " If Ran.Cells(d, c) > max Then max = Ran.Cells(d, c) "
trong ví du:
Public Function LonNhat(Ran As Range)
Dim max As Double, v As Integer, d As Integer, c As Integer
max = Ran.Cells(1, 1)
For d = 1 To Ran.Rows.Count
For c = 1 To Ran.Columns.Count
If Ran.Cells(d, c) > max Then max = Ran.Cells(d, c)
Next c
Next d
v = Tim(max, Ran)
LonNhat = max
End Function
-------------------------------------------------------
[INFO1]Thông báo:
Vì topic này:
http://www.giaiphapexcel.com/forum/...ải-thích-các-code-đề-nghị-các-bạn-gửi-vào-đây
đã quá dài nên BQT đóng lại.
Nay tôi mở topic mới với cùng chủ đề: GIẢI THÍCH NHỮNG THẮC MẮC VỀ CODE
Các bạn nếu có nhu cầu giải thích code, vui lòng post tại đây nhé
NDU96081631

[/INFO1]
 
Chỉnh sửa lần cuối bởi điều hành viên:
Nhờ các a/c sửa giúp e đoạn code này với ạ. Vấn đề chính là khi e ấn nút SAVE thì nó chỉ lưu có câu lệnh đầu tiên.View attachment 226500View attachment 226499
Lý thuyết:
Nếu ListBox có nguồn được định nghĩa ListBox.RowSource là ... thì:
1. Nếu click vào một mục bất kỳ trong ListBox hoặc thay đổi ListBox.ListIndex bằng code thì:
- trước hết code của ListBox_Change được thực hiện nếu có.
- tiếp theo code của ListBox_Click được thực hiện nếu có.

2. Khi vùng RowSource trên sheet (là nguồn của ListBox) thay đổi thì ListBox được làm mới.
Khi ListBox được làm mới thì:
- nếu hiện hành không có mục nào được chọn (ListIndex = -1) thì không có code nào được thực hiện thêm.
- nếu hiện hành có mục nào đó được chọn (ListIndex > -1) thì của ListBox_Change được thực hiện nếu có, tiếp theo code của ListBox_Click được thực hiện nếu có.
-----
Trước hết xét code
Mã:
Private Sub lb_dulieu_Click()
    With Me
        If .lb_dulieu.ListIndex >= 0 Then _
            .txt_sanpham_2.Value = .lb_dulieu.List(lb_dulieu.ListIndex, 1)
            ....
            .txt_sotien_2.Value = .lb_dulieu.List(lb_dulieu.ListIndex, 16)
        End With
End Sub
Theo lôgíc thì tất cả các dòng chứ không chỉ dòng đầu tiên đều phải nằm trong IF ... END IF.

Tức bỏ ký tự "_" sau Then và thêm "End If" trước "End With"
------
Vùng dữ liệu nguồn cho lb_dulieu được bạn thiết lập là data_sp (lb_dulieu.RowSource = data_sp).

Ta xét một ví dụ cụ thể để có thể giải thích cụ thể. Giả sử bạn chọn trong lb_dulieu dòng có STT = 4, ứng với dòng 6 trên sheet SP. Code của lb_dulieu_Click sẽ được thực hiện, hậu quả là các giá trị của dòng được chọn sẽ được nhập vào các textbox trên Form. Bây giờ giả sử bạn chỉnh sửa các giá trị trong các textbox bằng cách thêm hậu tố " mới". Tiếp theo bạn nhấn nút SAVE thì code của btnchange_Click sẽ được thực hiện
Mã:
Private Sub btnchange_Click()
Dim dong_sua As Long
    dong_sua = Sheets("SP").Range("A" & lb_dulieu.ListIndex + 2).Row
        With Worksheets("SP")
            .Cells(dong_sua, 2).Value = Dulieu_SP.txt_sanpham_2
            .Cells(dong_sua, 3).Value = Dulieu_SP.combo_ma_KH_2
            .Cells(dong_sua, 4).Value = Dulieu_SP.combo_nhucau_2
            .Cells(dong_sua, 5).Value = Dulieu_SP.combo_mucdogap_2
            .Cells(dong_sua, 6).Value = txt_add_2
            .Cells(dong_sua, 7).Value = txt_duan_2
            .Cells(dong_sua, 8).Value = txt_solo_2
            .Cells(dong_sua, 9).Value = txt_sothua_2
            .Cells(dong_sua, 10).Value = txt_soto_2
            .Cells(dong_sua, 11).Value = txt_dientich_2
            .Cells(dong_sua, 12).Value = txt_loaidat_2
            .Cells(dong_sua, 13).Value = txt_huong_2
            .Cells(dong_sua, 14).Value = txt_kichthuoc_2
            .Cells(dong_sua, 15).Value = txt_congtrinh_2
            .Cells(dong_sua, 16).Value = combo_cachtinh_2
            .Cells(dong_sua, 17).Value = txt_sotien_2
            .Cells(dong_sua, 18).Value = .Cells(dong_sua, 18).Value & "Edit by:" & Sheets("menu").Range("F2").Text & "; Edit Time: " & Now()
          End With
End Sub
Sau khi thực hiện dòng đầu tiên
Mã:
.Cells(dong_sua, 2).Value = Dulieu_SP.txt_sanpham_2
Thì trên sheet ô B6 sẽ có giá trị B6 = "THU.NT-4 mới".
Do dữ liệu trong vùng dữ liệu nguồn data_sp thay đổi (B6) nên theo lý thuyết thì code của lb_dulieu_Click sẽ được thực hiện. Đó chính là code ở trên. Bạn nhìn kỹ thì sẽ thấy là giá trị của các textbox mà bạn vừa bỏ nhiều công sức để thêm " mới" đã bị thay thế bằng các giá trị của lb_dulieu, tức lấy từ nguồn data_sp trên sheet do lb_dulieu được làm mới khi data_sp thay đổi Mà trên sheet mới chỉ có B6 thay đổi thôi, còn các giá trị khác chưa có " mới". Tức sau khi thực hiện lb_dulieu_Click thì chỉ có txt_sanpham_2 là có hậu tố " mới" do lấy từ cột 2 hàng 5 (ứng với B6 trên sheet) của lb_dulieu. Các textbox khác không có hậu tố " mới". Chúng có các giá trị y như sau khi chọn dòng STT = 4 và trước khi chỉnh sửa. Vì thế khi code thực thi các dòng còn lại bắt đầu từ dòng
Mã:
.Cells(dong_sua, 3).Value = Dulieu_SP.combo_ma_KH_2
tới cuối thì do các textbox không chứa các hậu tố " mới" mà chỉ chứa các giá trị y như trên sheet nên hiển nhiên là các giá trị trên sheet được thay bằng chính chúng nên không thể có hậu tố " mới" được.

Nếu có nhu cầu chỉnh sửa trên sheet thì không được phép chỉnh sửa trong vùng RowSource của ListBox. Nếu cần chỉnh sửa trong một vùng mà vùng đó đồng thời là dữ liệu nguồn cho ListBox thì phải cẩn thận khi dùng ListBox_Change và ListBox_Click. Nếu trong ListBox_Click code thay đổi giá trị của các textbox như của bạn, tức chúng được thay đổi - lấy từ vùng nguồn của ListBox, thì công lao chỉnh sửa các textbox trước đó đổ xuống sông xuống biển. Tôi hiểu là code Click phải thế để lấy các giá trị của dòng được chọn trong ListBox vào các textbox. Vậy thì phải từ bỏ dùng RowSource và dùng List để nhập dữ liệu vào ListBox.

Tóm lại:
0. Thêm End If vào lb_dulieu_Click
1. Xóa lb_dulieu.RowSource.
2. Thêm code vào Form
Mã:
Private Sub UserForm_Initialize()
    lb_dulieu.List = Worksheets("SP").Range("data_sp").Value
End Sub
3. Đễ làm mới lb_dulieu sau khi ghi xuống sheet thì trong Sub btnchange_Click sau dòng End With thì thêm code
Mã:
lb_dulieu.List = Worksheets("SP").Range("data_sp").Value
 
Upvote 0
Lý thuyết:
Nếu ListBox có nguồn được định nghĩa ListBox.RowSource là ... thì:
1. Nếu click vào một mục bất kỳ trong ListBox hoặc thay đổi ListBox.ListIndex bằng code thì:
- trước hết code của ListBox_Change được thực hiện nếu có.
- tiếp theo code của ListBox_Click được thực hiện nếu có.

2. Khi vùng RowSource trên sheet (là nguồn của ListBox) thay đổi thì ListBox được làm mới.
Khi ListBox được làm mới thì:
- nếu hiện hành không có mục nào được chọn (ListIndex = -1) thì không có code nào được thực hiện thêm.
- nếu hiện hành có mục nào đó được chọn (ListIndex > -1) thì của ListBox_Change được thực hiện nếu có, tiếp theo code của ListBox_Click được thực hiện nếu có.
-----
Trước hết xét code
Mã:
Private Sub lb_dulieu_Click()
    With Me
        If .lb_dulieu.ListIndex >= 0 Then _
            .txt_sanpham_2.Value = .lb_dulieu.List(lb_dulieu.ListIndex, 1)
            ....
            .txt_sotien_2.Value = .lb_dulieu.List(lb_dulieu.ListIndex, 16)
        End With
End Sub
Theo lôgíc thì tất cả các dòng chứ không chỉ dòng đầu tiên đều phải nằm trong IF ... END IF.

Tức bỏ ký tự "_" sau Then và thêm "End If" trước "End With"
------
Vùng dữ liệu nguồn cho lb_dulieu được bạn thiết lập là data_sp (lb_dulieu.RowSource = data_sp).

Ta xét một ví dụ cụ thể để có thể giải thích cụ thể. Giả sử bạn chọn trong lb_dulieu dòng có STT = 4, ứng với dòng 6 trên sheet SP. Code của lb_dulieu_Click sẽ được thực hiện, hậu quả là các giá trị của dòng được chọn sẽ được nhập vào các textbox trên Form. Bây giờ giả sử bạn chỉnh sửa các giá trị trong các textbox bằng cách thêm hậu tố " mới". Tiếp theo bạn nhấn nút SAVE thì code của btnchange_Click sẽ được thực hiện
Mã:
Private Sub btnchange_Click()
Dim dong_sua As Long
    dong_sua = Sheets("SP").Range("A" & lb_dulieu.ListIndex + 2).Row
        With Worksheets("SP")
            .Cells(dong_sua, 2).Value = Dulieu_SP.txt_sanpham_2
            .Cells(dong_sua, 3).Value = Dulieu_SP.combo_ma_KH_2
            .Cells(dong_sua, 4).Value = Dulieu_SP.combo_nhucau_2
            .Cells(dong_sua, 5).Value = Dulieu_SP.combo_mucdogap_2
            .Cells(dong_sua, 6).Value = txt_add_2
            .Cells(dong_sua, 7).Value = txt_duan_2
            .Cells(dong_sua, 8).Value = txt_solo_2
            .Cells(dong_sua, 9).Value = txt_sothua_2
            .Cells(dong_sua, 10).Value = txt_soto_2
            .Cells(dong_sua, 11).Value = txt_dientich_2
            .Cells(dong_sua, 12).Value = txt_loaidat_2
            .Cells(dong_sua, 13).Value = txt_huong_2
            .Cells(dong_sua, 14).Value = txt_kichthuoc_2
            .Cells(dong_sua, 15).Value = txt_congtrinh_2
            .Cells(dong_sua, 16).Value = combo_cachtinh_2
            .Cells(dong_sua, 17).Value = txt_sotien_2
            .Cells(dong_sua, 18).Value = .Cells(dong_sua, 18).Value & "Edit by:" & Sheets("menu").Range("F2").Text & "; Edit Time: " & Now()
          End With
End Sub
Sau khi thực hiện dòng đầu tiên
Mã:
.Cells(dong_sua, 2).Value = Dulieu_SP.txt_sanpham_2
Thì trên sheet ô B6 sẽ có giá trị B6 = "THU.NT-4 mới".
Do dữ liệu trong vùng dữ liệu nguồn data_sp thay đổi (B6) nên theo lý thuyết thì code của lb_dulieu_Click sẽ được thực hiện. Đó chính là code ở trên. Bạn nhìn kỹ thì sẽ thấy là giá trị của các textbox mà bạn vừa bỏ nhiều công sức để thêm " mới" đã bị thay thế bằng các giá trị của lb_dulieu, tức lấy từ nguồn data_sp trên sheet do lb_dulieu được làm mới khi data_sp thay đổi Mà trên sheet mới chỉ có B6 thay đổi thôi, còn các giá trị khác chưa có " mới". Tức sau khi thực hiện lb_dulieu_Click thì chỉ có txt_sanpham_2 là có hậu tố " mới" do lấy từ cột 2 hàng 5 (ứng với B6 trên sheet) của lb_dulieu. Các textbox khác không có hậu tố " mới". Chúng có các giá trị y như sau khi chọn dòng STT = 4 và trước khi chỉnh sửa. Vì thế khi code thực thi các dòng còn lại bắt đầu từ dòng
Mã:
.Cells(dong_sua, 3).Value = Dulieu_SP.combo_ma_KH_2
tới cuối thì do các textbox không chứa các hậu tố " mới" mà chỉ chứa các giá trị y như trên sheet nên hiển nhiên là các giá trị trên sheet được thay bằng chính chúng nên không thể có hậu tố " mới" được.

Nếu có nhu cầu chỉnh sửa trên sheet thì không được phép chỉnh sửa trong vùng RowSource của ListBox. Nếu cần chỉnh sửa trong một vùng mà vùng đó đồng thời là dữ liệu nguồn cho ListBox thì phải cẩn thận khi dùng ListBox_Change và ListBox_Click. Nếu trong ListBox_Click code thay đổi giá trị của các textbox như của bạn, tức chúng được thay đổi - lấy từ vùng nguồn của ListBox, thì công lao chỉnh sửa các textbox trước đó đổ xuống sông xuống biển. Tôi hiểu là code Click phải thế để lấy các giá trị của dòng được chọn trong ListBox vào các textbox. Vậy thì phải từ bỏ dùng RowSource và dùng List để nhập dữ liệu vào ListBox.

Tóm lại:
0. Thêm End If vào lb_dulieu_Click
1. Xóa lb_dulieu.RowSource.
2. Thêm code vào Form
Mã:
Private Sub UserForm_Initialize()
    lb_dulieu.List = Worksheets("SP").Range("data_sp").Value
End Sub
3. Đễ làm mới lb_dulieu sau khi ghi xuống sheet thì trong Sub btnchange_Click sau dòng End With thì thêm code
Mã:
lb_dulieu.List = Worksheets("SP").Range("data_sp").Value

Cảm ơn a Batman1 rất nhiều. đúng cái e cần luôn. Vấn đề chính của e là dùng RowSource nên sửa được 1 ô là nó cập nhật lại luôn.
 
Upvote 0
Cảm ơn a Batman1 rất nhiều. đúng cái e cần luôn. Vấn đề chính của e là dùng RowSource nên sửa được 1 ô là nó cập nhật lại luôn.
Nếu vẫn muốn dùng RowSource thì phải thay lb_dulieu_Click bằng lb_dulieu_DblClick. Lúc đó trong quá trình ghi xuống sheet thì ListBox vẫn được làm mới liên tục nhưng do DblClick không được gọi (chỉ cọ Click được gọi) nên các textbox không bị chuyển về các giá trị cũ lấy từ ListBox (lấy từ sheet).
Nhưng tôi không thích cái đỏ đỏ. Dùng List và khi nào cần làm mới ListBox thì mới gọi code làm mới thôi.
 
Upvote 0
Các anh/ chị cho e hỏi, có cách nào chỉnh được như comment trong hình ko vậy anh/chị?
Em mới tập tành VBA nên cũng ko rõ lắm. Cám ơn anh/chị trước nhé.


1571406723722.png
 

File đính kèm

  • NTP-NOTE.xlsm
    166.3 KB · Đọc: 6
Upvote 0
Các anh/ chị cho e hỏi, có cách nào chỉnh được như comment trong hình ko vậy anh/chị?
Em mới tập tành VBA nên cũng ko rõ lắm. Cám ơn anh/chị trước nhé.


View attachment 226867
Mình nghĩ ra 2 cách bạn xem dùng được thì dùng:
Cách 1: Thay đổi công thức ở cột C thành: =+IF(-TODAY()+E2<0;"Hoàn thành";IF(-TODAY()+E2=0;"Đến hạn";"Còn "&E2-TODAY()&" ngày"));
Cách 2: thay đoạn code:
'Loc Phát Sinh Dén Han: '
...
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3)
....
Thành code:
If Cells(j, 3) = ChrW(272) & ChrW(7871) & "n h" & ChrW(7841) & "n" Then
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3)
Else: Me.ListDenHan.List(ListDenHanRow, 2) = "Còn " & Cells(j, 3) & "ngày"
End If

Bổ sung cách 3 ngắn và đơn giản nhất:
thay code:
'Loc Phát Sinh Dén Han: '
...
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3)
....
Thành code:
'Loc Phát Sinh Dén Han: '
...
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3).text
....
 
Lần chỉnh sửa cuối:
Upvote 0
Mình nghĩ ra 2 cách bạn xem dùng được thì dùng:
Cách 1: Thay đổi công thức ở cột C thành: =+IF(-TODAY()+E2<0;"Hoàn thành";IF(-TODAY()+E2=0;"Đến hạn";"Còn "&E2-TODAY()&" ngày"));
Cách 2: thay đoạn code:
'Loc Phát Sinh Dén Han: '
...
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3)
....
Thành code:
If Cells(j, 3) = ChrW(272) & ChrW(7871) & "n h" & ChrW(7841) & "n" Then
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3)
Else: Me.ListDenHan.List(ListDenHanRow, 2) = "Còn " & Cells(j, 3) & "ngày"
End If

Bổ sung cách 3 ngắn và đơn giản nhất:
thay code:
'Loc Phát Sinh Dén Han: '
...
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3)
....
Thành code:
'Loc Phát Sinh Dén Han: '
...
Me.ListDenHan.List(ListDenHanRow, 2) = Cells(j, 3).text
....
Em đã làm được, cám ơn bobokiki132 nhiều nha.
 
Upvote 0
Em chào các anh chị ạ,
Em đang là học sinh cấp 2, và đang tìm tòi học VBA ạ,

Em đang chưa biết làm 1 bài toán như này, mong a chị gợi ý thuật toán giúp em ạ:

Gán vào 1 cell trong sheet, 1 giá trị (bằng số) - (ví dụ gán vào cell (1,1) giá trị bằng 10), cứ sau 2' thì giá trị trong cell (1,1) cộng thêm 1

-----------
Em đã tìm google mấy ngày nay cả web tiếng việt và tiếng anh nhưng ko ra cách làm ạ (e ko giỏi tiếng anh cho lắm ^^), mong anh chị chỉ giáo ạ

Em cám ơn nhiều ạ !
 
Upvote 0
Mới tìm tòi học mà vọc chi cái rắc rối vậy? Cái này rất ít liên quan đến thuật toán lập trình, nó chỉ là một xảo thuật.

Từ khoá để tìm: Appplication.OnTime (tìm ở GPE cũng được, có cả đống)

Chú: nếu còn muốn hỏi tiếp thì nên học thói quen viết trọn từ, tránh viết tắt. Chịu khó để ý sẽ thấy những người trả lời rất hiếm khi viết tắt.
 
Upvote 0
Em chào các anh chị ạ,
Em đang là học sinh cấp 2, và đang tìm tòi học VBA ạ,

Em đang chưa biết làm 1 bài toán như này, mong a chị gợi ý thuật toán giúp em ạ:

Gán vào 1 cell trong sheet, 1 giá trị (bằng số) - (ví dụ gán vào cell (1,1) giá trị bằng 10), cứ sau 2' thì giá trị trong cell (1,1) cộng thêm 1

-----------
Em đã tìm google mấy ngày nay cả web tiếng việt và tiếng anh nhưng ko ra cách làm ạ (e ko giỏi tiếng anh cho lắm ^^), mong anh chị chỉ giáo ạ

Em cám ơn nhiều ạ !
Thử chơi.À mà nó không dừng đâu nhé.
 

File đính kèm

  • Book1.xlsm
    13.3 KB · Đọc: 9
Upvote 0
Các thầy/anh ơi, mình có thể tạo ra Function mà có thể gọi tên sheet, hoặc tạo ra 1 mảng tên các sheet được ko ạ?
 
Upvote 0
Các thầy/anh ơi, mình có thể tạo ra Function mà có thể gọi tên sheet, hoặc tạo ra 1 mảng tên các sheet được ko ạ?
Bạn thử.
Mã:
Function tensheet(ByVal so As Integer) As String
        tensheet = Sheets(so).Name
End Function
Mã:
=tensheet(1)
 
Upvote 0
Bạn kéo nó dài ra nhé.Cái này nó chỉ hiện tên 1 sheets thôi mà.
em muốn kết hợp công thức mảng vào cái hàm tensheet đó anh để tạo ra mảng {TH, ab, cd, xy} sau đó sẽ dùng tiếp hàm indirect để giải quyết theo mục đích
Cơ mà =tensheet{1,2,3,4} nó báo lỗi value
Có phải function tạo ra không thể tạo mảng nhưng trong công thức excel phải ko ạ?
 
Lần chỉnh sửa cuối:
Upvote 0
em muốn kết hợp công thức mảng vào cái hàm tensheet đó anh để tạo ra mảng {TH, ab, cd, xy} sau đó sẽ dùng tiếp hàm indirect để giải quyết theo mục đích
Cơ mà =tensheet{1,2,3,4} nó báo lỗi value
Có phải function tạo ra không thể tạo mảng nhưng trong công thức excel phải ko ạ?
Bạn thử cái hàm mảng này.
 

File đính kèm

  • Book1.xlsm
    16.1 KB · Đọc: 10
Upvote 0
Kính nhờ các anh chị chỉ giúp mình 1 đoạn code về tìm ngày trong dữ liệu.
Giả sử cột A chứa dữ liệu dạng Date, format dd/mm/yyyy, sắp xếp tăng dần (các giá trị có thể không liên tục, có thể có nhiều dòng có cùng giá trị).
Có thể dùng code Function sau
Function TimDong(arr As Range, gt As Date) As Integer
Dim k As Range
For Each k In arr
If k.Value >= gt Then
TimDong = k.Row
Exit Function
End If
Next
End Function

Không cần bẫy lỗi vì nếu không có thì cho KQ = 0.
 
Upvote 0
chào các anh. các anh cho em hỏi cái nyaf là em bị lồi gì ạ

Private Sub Textcaphanmem_Change()
On Error Resume Next
Textcaphanmem.Text = Format(Textcaphanmem.Text, "#,##0")
End Sub

em dùng hàm này trong userfrom mà trong textbox thì hiện đúng nhưng trong file nhập excel thì bị thiều mất 3 số 0
VD như trong textbox nhập 2000000 textbox sẽ hiện 2,000,000 trong execl là 2.000.000
nhưng khi nhập 200000 texbox hiện 200,000 exel chỉ hiện 200
em đã chỉnh lại trong Format cells/Number/Custome #,##0 đung như trong textbox rồi :(
 
Upvote 0
Help Me!!!
Kính gửi các anh chị trên diễn đàn mình, em mới học VBA nên chưa biết sửa code đúng cách, rất mong các anh chị chỉ giáo giúp em.
chẳng là e muốn lấy danh sách những ông bà (gồm tất cả các cột như trong file data1) mà có giá trị tại cột PENINT và/hoăc cột BILINT và/hoặc cột BILPRN #0 sang báo cáo tại file final. Tức là nếu giá trị tại 3 cột trền đều bằng 0 thì e ko lấy.
E đã viết code, nhưng mắc lỗi và không biết sửa.
P/s: Các anh/chị có thể thay đổi đương dẫn trong code để mở file data1 ạ.
Mong các anh chị xem sớm giúp e với, em cảm ơn rất rất nhiều ạ.
 

File đính kèm

  • DATA1.xlsx
    9.5 KB · Đọc: 10
  • final.xlsm
    19.8 KB · Đọc: 7
Lần chỉnh sửa cuối:
Upvote 0
Web KT
Back
Top Bottom