kivymd / KivyMD

KivyMD is a collection of Material Design compliant widgets for use with Kivy, a framework for cross-platform, touch-enabled graphical applications. https://youtube.com/c/KivyMD https://twitter.com/KivyMD https://habr.com/ru/users/kivymd https://stackoverflow.com/tags/kivymd
https://kivymd.readthedocs.io
MIT License
2.26k stars 674 forks source link

Issue with VKeyboard - Inaccurate Key Presses and Input Handling with KivyMD TextField #1753

Open dcl920108 opened 6 days ago

dcl920108 commented 6 days ago

Description: I am encountering an issue using VKeyboard in my KivyMD application where the virtual key presses do not register correctly, and the input values are not being entered properly into the KivyMD TextField.

Here are the main issues:

  1. Inaccurate Key Press Registration:
    • When pressing a key on the virtual keyboard, the key that is actually displayed or registered is different from the key that was pressed.
    • For example, when pressing "4" on the virtual keyboard, the application sometimes displays "6" instead.

130926

  1. Failure to Input Correct Values into TextField:
    • The values from VKeyboard are not properly reflected in the KivyMD TextField.
    • Despite capturing the key press event (on_key_up), the value does not appear correctly in the input field, or it does not get entered at all.

Code Extract:

Below is the part of the code dealing with the virtual keyboard (VKeyboard) and the KivyMD TextField:

{
    "title" : "Numeric",
    "description" : "A numeric keypad",
    "cols" : 3,
    "rows": 4,
    "normal_1": [
    ["7", "7", "7", 1],
    ["8", "8", "8", 1],
    ["9", "9", "9", 1]],
    "normal_2": [
    ["4", "4", "4", 1],
    ["5", "5", "5", 1],
    ["6", "6", "6", 1]],
    "normal_3": [
    ["1", "1", "1", 1],
    ["2", "2", "2", 1],
    ["3", "3", "3", 1]],
    "normal_4": [
    ["0", "0", "0", 1],
    [".", ".", ".", 1],
    ["\u232b", null, "backspace", 1]],
    "shift_1": [
    ["7", "7", "7", 1],
    ["8", "8", "8", 1],
    ["9", "9", "9", 1]],
    "shift_2": [
    ["4", "4", "4", 1],
    ["5", "5", "5", 1],
    ["6", "6", "6", 1]],
    "shift_3": [
    ["1", "1", "1", 1],
    ["2", "2", "2", 1],
    ["3", "3", "3", 1]],
    "shift_4": [
    ["0", "0", "0", 1],
    [".", ".", ".", 1],
    ["\u232b", null, "backspace", 1]]
    }

Issues Summary:

  1. Key Press Mismatch:

    • When pressing a key on the VKeyboard, the displayed or registered key is incorrect. For example, pressing "4" may result in "6" being registered.
  2. Input Issue with TextField:

    • Even though I am capturing key presses through the on_key_up event, the value is not correctly entered into the KivyMD TextField.

Expected Behavior:

Actual Behavior:

Environment:

Steps to Reproduce:

  1. Set up VKeyboard with a numeric layout (numeric.json) as shown above.
  2. Bind the VKeyboard to a TextField using the focus event (on_focus) to control the visibility of the virtual keyboard.
  3. Run the application and attempt to input numbers through the virtual keyboard.
  4. Observe that the key that is pressed is often incorrectly registered or not properly reflected in the TextField.

Additional Information:

I would appreciate any advice or suggestions on how to resolve this issue. Thank you!

dcl920108 commented 5 days ago

Minimal Reproducible Example

This script is a minimal reproducible example of a KivyMD-based graphical user interface (GUI) application. It demonstrates the following features:

  1. Light-themed User Interface: The app uses a light theme with a green primary palette for a clean and simple look.
  2. Main Screen and Navigation: It consists of two main screens - a main screen and an isothermal control screen, managed by a screen manager for easy navigation.
  3. Input Fields with Virtual Keyboard: Users can input temperature, cycle count, and cycle time using text fields. The app uses a virtual numeric keyboard (VKeyboard) that appears when a text field is focused.
  4. Dynamic Layout: The GUI contains multiple buttons for navigation, configuration, and switching between different screens. It also features a back button for easy navigation.
  5. Date-Time Display: The app displays the current date and time, which updates every second.
  6. Button Actions: Buttons such as "Thermal Cycle", "Isothermal", and "Confirm Settings" are provided for user interactions, but the backend functionality is not implemented in this minimal example.

The main purpose of this example is to show how to create a user interface with KivyMD that includes interactive elements such as buttons, text fields, and a virtual keyboard, while ensuring the interface is responsive and user-friendly.


from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.button import MDButton, MDButtonIcon, MDButtonText
from kivymd.uix.label import MDLabel
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.fitimage import FitImage
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.uix.textfield import (
    MDTextField,
    MDTextFieldLeadingIcon,
    MDTextFieldHintText,
    MDTextFieldHelperText,
)
from kivy.uix.vkeyboard import VKeyboard
from kivy.clock import Clock
from datetime import datetime

class MainScreen(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        # 设置浅色主题和主色调
        self.theme_cls = MDApp.get_running_app().theme_cls
        self.theme_cls.theme_style = "Light"
        self.theme_cls.primary_palette = "Green"

        # 设置屏幕背景颜色
        self.md_bg_color = (1, 1, 1, 1)

        # 创建整体布局
        layout = MDBoxLayout(orientation="horizontal")
        self.left_layout = MDFloatLayout(size_hint=(0.4, 1))
        self.right_layout = MDFloatLayout(size_hint=(0.6, 1))

        qpcr_button = MDButton(
            MDButtonIcon(icon="ruler-square-compass"),
            MDButtonText(text="Thermal Cycle", font_style="Title"),
            style="elevated",
            pos_hint={"center_x": 0.7, "center_y": 0.6},
            height="200dp",
            size_hint=(3.2, 0.8)  # size_hint=(0.4, 0.1)
        )

        iso_button = MDButton(
            MDButtonIcon(icon="liquor"),
            MDButtonText(text="      Isothermal      ", font_style="Title"),
            style="elevated",
            pos_hint={"center_x": 1.8, "center_y": 0.6},
            height="200dp",
            size_hint=(6.4, 1.6)  # size_hint=(0.4, 0.1)
        )
        iso_button.bind(on_press=self.switch_to_isothermal)  # 绑定切换屏幕的方法

        # 将按钮添加到左侧布局
        self.left_layout.add_widget(qpcr_button)
        self.left_layout.add_widget(iso_button)

        # 创建底部的时间标签
        self.date_time_label = MDLabel(
            text="YYYY-MM-DD HH:MM:SS",
            halign="left",
            size_hint=(None, None),
            size=(dp(200), dp(40)),
            pos_hint={"x": -0.65, "y": 0.01},
        )

        # 将时间和 Logo 添加到右侧布局
        self.right_layout.add_widget(self.date_time_label)

        # 将左右布局添加到整体布局中
        layout.add_widget(self.left_layout)
        layout.add_widget(self.right_layout)

        # 将整体布局添加到屏幕中
        self.add_widget(layout)

        # 定期更新时间
        Clock.schedule_interval(self.update_date_time, 1)

    def update_date_time(self, dt):
        now = datetime.now()
        self.date_time_label.text = now.strftime("%Y-%m-%d %H:%M:%S")

    def switch_to_isothermal(self, *args):
        # 切换到名为 'isothermal' 的屏幕
        if self.manager:
            print("Switching to AGD screen...")  # 添加调试信息
            self.manager.current = "isothermal"

class MotorControlScreen(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.locked = False
        self.data_records = []
        self.temperature_records = []  # 用于存储温度记录
        self.current_cycle = 0
        self.total_cycles = 20
        self.num_samples = 1000
        self.countdown_time = 360
        self.image_widget = None
        self.show_temperature_plot = False

        # 创建 VKeyboard 对象
        self.keyboard = VKeyboard()
        self.keyboard.layout_path = "./"  # 设置键盘布局文件的路径(假设布局文件位于当前目录下)
        self.keyboard.layout = 'numeric.json'  # 只使用数字键盘布局
        self.keyboard.size_hint = (1, 0.3)
        self.keyboard.pos_hint = {"center_x": 0, "y": 0}
        self.keyboard.bind(on_key_up=self.on_key_up)
        print("Keyboard binding successful")  # 添加调试信息

        # 调用 build_ui() 并将生成的 screen 作为主界面
        self.add_widget(self.build_ui())

    def build_ui(self):
        screen = MDScreen(md_bg_color=(1, 1, 1, 1))
        layout = MDBoxLayout(orientation="horizontal")
        self.left_layout = MDFloatLayout(size_hint=(0.4, 1))
        self.right_layout = MDFloatLayout(size_hint=(0.6, 1))

        back_button = MDButton(
            MDButtonIcon(icon="arrow-left"),  # 设置图标为返回箭头
            MDButtonText(text="Back", font_style="Title"),
            style="elevated",
            pos_hint={"center_x": 2.2, "center_y": 0.95},  # 左上角位置
            size_hint=(0.25, 0.1),
            on_release=self.switch_to_main_screen
        )

        toggle_plot_button = MDButton(
            MDButtonIcon(icon="swap-horizontal"),
            MDButtonText(text="Switching chart"),
            style="elevated",
            pos_hint={"center_x": -0.35, "center_y": 0.2},
            height="56dp",
            size_hint_x=0.6
        )

        # 添加温度输入框
        self.temperature_input = MDTextField(
            MDTextFieldLeadingIcon(icon="thermometer"),
            MDTextFieldHintText(text="Target Temperature (°C)"),
            MDTextFieldHelperText(text="Please enter the target temperature (e.g., 98)", mode="persistent"),
            mode="outlined",
            size_hint_x=None,
            width="240dp",
            pos_hint={"center_x": 0.5, "center_y": 0.9},
        )
        self.temperature_input.bind(focus=self.show_keyboard)

        # 添加循环次数输入框
        self.cycle_count_input = MDTextField(
            MDTextFieldLeadingIcon(icon="repeat"),
            MDTextFieldHintText(text="Cycle Count"),
            MDTextFieldHelperText(text="Please enter the number of cycles (e.g., 20)", mode="persistent"),
            mode="outlined",
            size_hint_x=None,
            width="240dp",
            pos_hint={"center_x": 0.5, "center_y": 0.7},
        )
        self.cycle_count_input.bind(focus=self.show_keyboard)

        # 添加每个循环时间输入框
        self.cycle_time_input = MDTextField(
            MDTextFieldLeadingIcon(icon="timer"),
            MDTextFieldHintText(text="Cycle Time (seconds)"),
            MDTextFieldHelperText(text="Please enter the time for each cycle (e.g., 60)", mode="persistent"),
            mode="outlined",
            size_hint_x=None,
            width="240dp",
            pos_hint={"center_x": 0.5, "center_y": 0.5},
        )
        self.cycle_time_input.bind(focus=self.show_keyboard)

        # 确认按钮,确认设定并开始实验流程
        confirm_button = MDButton(
            MDButtonIcon(icon="check"),
            MDButtonText(text="Confirm Settings"),
            style="elevated",
            pos_hint={"center_x": 0.5, "center_y": 0.3},
            height="56dp",
            size_hint_x=0.6
        )

        # 绑定按钮到 toggle_plot() 函数,用于切换显示的图表
        toggle_plot_button.bind(on_press=self.toggle_plot)

        self.left_layout.add_widget(self.temperature_input)
        self.left_layout.add_widget(self.cycle_count_input)
        self.left_layout.add_widget(self.cycle_time_input)
        self.left_layout.add_widget(confirm_button)
        self.left_layout.add_widget(back_button)

        self.right_layout.add_widget(toggle_plot_button)
        self.right_layout.add_widget(self.keyboard)

        layout.add_widget(self.left_layout)
        layout.add_widget(self.right_layout)

        screen.add_widget(layout)

        return screen

    def show_keyboard(self, instance, value):
        if value:  # 当输入框被聚焦时显示键盘
            self.keyboard.opacity = 1
            instance.focus = True  # 确保输入框获得焦点
        else:  # 当失去焦点时隐藏键盘
            self.keyboard.opacity = 0

    def on_key_up(self, keyboard, keycode, *args):
        print(f"Key pressed: {keycode}")  # 调试信息

        # 确保 keycode 至少包含两个元素,避免索引错误
        if len(keycode) < 2:
            return

        # 检查哪个输入框当前处于聚焦状态
        if self.temperature_input.focus:
            print("Temperature input focused")
        elif self.cycle_count_input.focus:
            print("Cycle count input focused")
        elif self.cycle_time_input.focus:
            print("Cycle time input focused")

        # 将输入的值添加到相应的输入框中
        if keycode[1] == 'backspace':
            # 如果按下的是回删键,则删除输入框中的最后一个字符
            if self.temperature_input.focus:
                self.temperature_input.text = self.temperature_input.text[:-1]
            elif self.cycle_count_input.focus:
                self.cycle_count_input.text = self.cycle_count_input.text[:-1]
            elif self.cycle_time_input.focus:
                self.cycle_time_input.text = self.cycle_time_input.text[:-1]
        elif keycode[1].isdigit() or keycode[1].isalpha():
            # 如果按下的是数字键或字母键,则将字符添加到相应的输入框
            if self.temperature_input.focus:
                self.temperature_input.text += keycode[1]
            elif self.cycle_count_input.focus:
                self.cycle_count_input.text += keycode[1]
            elif self.cycle_time_input.focus:
                self.cycle_time_input.text += keycode[1]

    def switch_to_main_screen(self, instance):
        if self.manager:
            self.manager.current = "main"

    def toggle_plot(self, instance):
        self.show_temperature_plot = not self.show_temperature_plot

class MainApp(MDApp):
    def build(self):
        # 创建 ScreenManager 来管理所有界面
        sm = MDScreenManager()

        # 添加主界面
        main_screen = MainScreen(name="main")
        sm.add_widget(main_screen)

        # 添加等温控制界面
        isothermal_screen = MotorControlScreen(name="isothermal")
        sm.add_widget(isothermal_screen)

        # 设置初始显示的界面
        sm.current = "main"

        return sm

# 运行应用程序
if __name__ == "__main__":
    MainApp().run()