2025, Oct 22 23:31

QTreeWidget में अवांछित संपादन रोकें: edit() ओवरराइड से कॉलम-आधारित नियंत्रण

PyQt5 QTreeWidget में Tab के साथ अवांछित कॉलम संपादन रोकें। जानें keyPressEvent क्यों विफल होता है और edit() ओवरराइड से व्यू स्तर पर भरोसेमंद नियंत्रण पाएं.

QTreeWidget में अवांछित संपादन रोकना पहली नज़र में आसान लगता है, जब तक कि आप यह न देख लें कि QTreeWidgetItem को कीबोर्ड इवेंट्स बिल्कुल दिखाई ही नहीं देते। यदि आप चाहते हैं कि Tab से फोकस बदले, लेकिन अन्य कॉलम में तुरंत संपादन रोका जाए, तो सीधा-सादा तरीका काम नहीं करेगा। यहां इसके पीछे का कारण संक्षेप में समझाया गया है और ऐसी भरोसेमंद विधि दी गई है जो संपादन नियमों को वास्तव में लागू करती है।

समस्या को पुन: उत्पन्न करना

नीचे दिया गया सेटअप संदर्भ मेनू से नई पंक्ति बनाने देता है, “Header 1” कॉलम पर एडिटिंग शुरू करता है और टैब नेविगेशन सक्षम करता है। Tab दबाने पर फोकस तो बदलता है, लेकिन अगला कॉलम संपादन योग्य बना रहता है — यही व्यवहार हमें नहीं चाहिए।

from PyQt5 import QtCore, QtGui, QtWidgets

class NodeItem(QtWidgets.QTreeWidgetItem):
    def __init__(self, parent=None):
        QtWidgets.QTreeWidgetItem.__init__(self, parent)

    def keyPressEvent(self, ev):
        if ev.key() == QtCore.Qt.Key_Tab:
            self.setFlags(self.flags() | ~QtCore.Qt.ItemIsEditable)
            print('not working')

def make_view():
    tw = QtWidgets.QTreeWidget()

    tw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    tw.customContextMenuRequested.connect(showMenu)
    
    tw.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    tw.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
    tw.setTabKeyNavigation(True)
    
    tw.headerItem().setText(0, "Header 0")
    tw.headerItem().setText(1, "Header 1")
    tw.headerItem().setText(2, "Header 2")
        
    return tw

def showMenu(pos):
    menu = QtWidgets.QMenu()
    action = menu.addAction('add item', insertNode)
    menu.exec_(view.mapToGlobal(pos))

def insertNode():
    it = NodeItem(view)
    it.setFlags(it.flags() | QtCore.Qt.ItemIsEditable)
    view.editItem(it, 1)

if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle("fusion")
    view = make_view()
    view.show()
    sys.exit(app.exec_())

असल में हो क्या रहा है

QTreeWidgetItem कोई विजेट नहीं है और वह यूज़र इनपुट को हैंडल नहीं करता। Tab जैसे कीबोर्ड इवेंट्स को व्यू और डेलीगेट्स संसाधित करते हैं, आइटम नहीं; इसलिए आइटम पर keyPressEvent() दोबारा परिभाषित करने पर वह कभी कॉल ही नहीं होगा। यही मूल कारण है कि Tab को आइटम के अंदर इंटरसेप्ट करने की कोशिश चुपचाप विफल हो जाती है।

एक और बारीकी है: editItem() संपादन तो शुरू कर देता है, लेकिन current index सेट नहीं करता। यदि आप नेविगेशन नियंत्रित करना चाहते हैं या current index पर निर्भर हैं, तो setCurrentItem(item, column) के साथ उसे स्पष्ट रूप से सेट करना होगा। साथ ही, अगर आप यह सुनिश्चित करना चाहते हैं कि उपयोगकर्ता किसी भी तरीके से संपादन शुरू करे, अन्य कॉलम संपादन योग्य न हों, तो Tab केवल लक्षण है, मूल कारण नहीं। नियम लागू करने की सही जगह व्यू की एडिटिंग पाइपलाइन है।

अंत में, यदि आप फ्लैग हटाना चाहते हैं, तो सही बिटवाइज़ रूप है flags() & ~QtCore.Qt.ItemIsEditable, न कि flags() | ~QtCore.Qt.ItemIsEditable।

उपाय: संपादन-योग्यता को व्यू स्तर पर लागू करें

पुख्ता समाधान यह है कि QTreeWidget को सबक्लास करके edit() को ओवरराइड किया जाए। यदि दिया गया index उसी कॉलम का है जिसे आप संपादन योग्य रखना चाहते हैं, तो बेस इम्प्लीमेंटेशन को कॉल करें; अन्यथा False लौटाकर रोक दें। इससे नियम एक जगह केंद्रीकृत हो जाता है और डबल-क्लिक, कीबोर्ड या प्रोग्रामेटिक कॉल—सभी ट्रिगर्स पर समान रूप से लागू होता है।

from PyQt5 import QtCore, QtGui, QtWidgets

class NodeItem(QtWidgets.QTreeWidgetItem):
    def __init__(self, parent=None):
        QtWidgets.QTreeWidgetItem.__init__(self, parent)

class GuardedTree(QtWidgets.QTreeWidget):

    def __init__(self):
        super().__init__()

    def edit(self, index, trigger, event):
        if index.column() == 1:
            return super().edit(index, trigger, event)
        return False

def make_view():
    tw = GuardedTree()

    tw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    tw.customContextMenuRequested.connect(showMenu)
    
    tw.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    tw.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
    tw.setTabKeyNavigation(True)
    
    tw.setHeaderLabels(["Header 0", "Header 1", "Header 2"])
    
    return tw

def showMenu(pos):
    menu = QtWidgets.QMenu()
    action = menu.addAction('add item', insertNode)
    menu.exec_(view.viewport().mapToGlobal(pos))

def insertNode():
    it = NodeItem(view)
    it.setFlags(it.flags() | QtCore.Qt.ItemIsEditable)
    view.editItem(it, 1)

if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle("fusion")
    view = make_view()
    view.show()
    sys.exit(app.exec_())

यहां दो छोटे-से सुधार भी हैं। पहला, स्क्रॉल एरिया में customContextMenuRequested व्यूपोर्ट-कोऑर्डिनेट्स देता है, इसलिए मेनू को सही स्थान पर दिखाने के लिए view.viewport().mapToGlobal(pos) का उपयोग ज़रूरी है। दूसरा, हेडर लेबल्स को setHeaderLabels() के जरिए एक ही कॉल में सेट किया जा सकता है।

यह क्यों मायने रखता है

एडिटिंग नियमों के लिए आइटम-स्तरीय इवेंट हैंडलिंग पर भरोसा कमजोर होता है, क्योंकि आइटम को वे इवेंट मिलते ही नहीं। बाधा को व्यू की edit() में रखने से, चाहे एडिट कैसे भी ट्रिगर हो, व्यवहार सुसंगत रहता है और अलग-अलग इनपुट तरीकों से आने वाले किनारी मामलों से बचाव होता है। साथ ही, तर्क एक ही जगह रहता है, जिसे बनाए रखना और समझना आसान होता है।

सारांश और व्यावहारिक सलाह

यदि QTreeWidget में कॉलम-आधारित संपादन प्रतिबंध चाहिए, तो आइटम से नहीं, व्यू से नियंत्रण करें। edit() को ओवरराइड करें और केवल वही कॉलम अनुमति दें जिन्हें संपादन योग्य होना चाहिए। प्रोग्रामेटिक रूप से एडिट शुरू करते समय याद रखें कि editItem() current index नहीं बदलता; अगर वर्तमान चयन मायने रखता है तो setCurrentItem() कॉल करें। स्क्रॉल होने वाले व्यू में कॉन्टेक्स्ट मेनू के लिए, viewport की स्थिति को ग्लोबल कोऑर्डिनेट्स में मैप करें। और फ्लैग टॉगल करते समय, उन्हें साफ करने के लिए & ~ वाला सही बिटवाइज़ रूप इस्तेमाल करें। इस संयोजन से Tab के साथ नेविगेशन पूर्वानुमेय बनता है और बिना दुष्प्रभाव के अनचाहे संपादन विश्वसनीय रूप से रुकते हैं।

यह लेख StackOverflow के प्रश्न (लेखक: EGuy) और EGuy के उत्तर पर आधारित है।