[{"data":1,"prerenderedAt":1846},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fvalidate-plugin-dialog-input-pyqgis":3},{"id":4,"title":5,"body":6,"description":1835,"extension":1836,"meta":1837,"navigation":267,"path":1842,"seo":1843,"stem":1844,"__hash__":1845},"docs\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fvalidate-plugin-dialog-input-pyqgis\u002Findex.md","Validate Plugin Dialog Input in PyQGIS",{"type":7,"value":8,"toc":1822},"minimark",[9,13,17,26,178,183,215,219,222,347,362,373,377,380,731,755,758,824,837,841,844,1064,1081,1198,1202,1205,1356,1369,1373,1376,1541,1561,1565,1571,1663,1667,1727,1731,1740,1744,1750,1763,1773,1787,1791,1818],[10,11,5],"h1",{"id":12},"validate-plugin-dialog-input-in-pyqgis",[14,15,16],"p",{},"A plugin dialog that accepts anything and fails halfway through is worse than one that refuses to start. The user has already waited, something has probably been written to disk, and the error arrives as a traceback in a log they will not read. Validating up front costs a few dozen lines and turns every one of those failures into a sentence next to the field that caused it.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces",". It covers three layers of validation — Qt validators on the widget, live checks that gate the OK button, and a final check on accept — plus where to display the message so it is actually seen.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,97,100,104,109,116,121,125,128,131,134,137,141,146,150,153,156,159,162,170,174],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 316","img","Three layers of validation: a widget validator preventing bad characters, live checks gating the OK button, and a final check on accept catching what changed since","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Three layers, each catching what the previous cannot",[40,41,42],"desc",{},"A Qt validator on the widget prevents impossible input from being typed at all. Live validation runs on every change and enables or disables the OK button with an explanation. A final check on accept catches conditions that changed after the dialog opened, such as a layer being removed or a file appearing.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","316","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"valArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Each layer catches what the one before it cannot",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"20","60","228","140","10","#eff3ff","#2563eb","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"134","88","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","1 · widget validator",[68,93,96],{"x":88,"y":94,"style":95,"fill":66,"textAnchor":74},"114","text-anchor:middle;font-size:10px;font-family:sans-serif","letters in a number field",[68,98,99],{"x":88,"y":88,"style":95,"fill":66,"textAnchor":74},"simply cannot be typed",[68,101,103],{"x":88,"y":102,"style":95,"fill":84,"textAnchor":74},"162","no message needed",[68,105,108],{"x":88,"y":106,"style":95,"fill":107,"textAnchor":74},"184","#59645f","cheapest, narrowest",[110,111],"line",{"x1":112,"y1":113,"x2":114,"y2":113,"stroke":66,"style":115},"254","130","278","stroke-width:2;marker-end:url(#valArrow)",[44,117],{"x":118,"y":79,"width":80,"height":81,"rx":82,"fill":119,"stroke":120,"style":85},"286","#edf8e9","#15803d",[68,122,124],{"x":123,"y":89,"style":90,"fill":120,"textAnchor":74},"400","2 · live validation",[68,126,127],{"x":123,"y":94,"style":95,"fill":66,"textAnchor":74},"runs on every change",[68,129,130],{"x":123,"y":88,"style":95,"fill":66,"textAnchor":74},"enables or disables OK",[68,132,133],{"x":123,"y":102,"style":95,"fill":120,"textAnchor":74},"explains why, inline",[68,135,136],{"x":123,"y":106,"style":95,"fill":107,"textAnchor":74},"where most of it lives",[110,138],{"x1":139,"y1":113,"x2":140,"y2":113,"stroke":66,"style":115},"520","544",[44,142],{"x":143,"y":79,"width":106,"height":81,"rx":82,"fill":144,"stroke":145,"style":85},"552","#fdf2e2","#b45309",[68,147,149],{"x":148,"y":89,"style":90,"fill":145,"textAnchor":74},"644","3 · on accept",[68,151,152],{"x":148,"y":94,"style":95,"fill":66,"textAnchor":74},"the layer was removed",[68,154,155],{"x":148,"y":88,"style":95,"fill":66,"textAnchor":74},"the file now exists",[68,157,158],{"x":148,"y":102,"style":95,"fill":145,"textAnchor":74},"refuse to close",[68,160,161],{"x":148,"y":106,"style":95,"fill":107,"textAnchor":74},"catches races",[44,163],{"x":164,"y":165,"width":139,"height":166,"rx":58,"fill":167,"stroke":168,"style":169},"120","230","66","#fffdf7","#b91c1c","stroke-width:2",[68,171,173],{"x":70,"y":172,"style":90,"fill":168,"textAnchor":74},"256","what none of them replace",[68,175,177],{"x":70,"y":114,"style":176,"fill":66,"textAnchor":74},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","handling the failure when the work itself goes wrong",[179,180,182],"h2",{"id":181},"prerequisites","Prerequisites",[184,185,186,194,207],"ul",{},[187,188,189,193],"li",{},[190,191,192],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[187,195,196,197,201,202,206],{},"A dialog with a button box, ideally built from a ",[198,199,200],"code",{},".ui"," file — see ",[21,203,205],{"href":204},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fload-ui-file-at-runtime-pyqgis\u002F","loading a .ui file at runtime",".",[187,208,209,210,214],{},"Inputs worth validating; the ",[21,211,213],{"href":212},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fuse-qgsmaplayercombobox-in-plugin-dialog-pyqgis\u002F","layer and field combo boxes"," already validate their own type.",[179,216,218],{"id":217},"layer-one-stop-it-being-typed","Layer one: stop it being typed",[14,220,221],{},"The cheapest validation prevents impossible input at the keyboard.",[223,224,229],"pre",{"className":225,"code":226,"language":227,"meta":228,"style":228},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtGui import QDoubleValidator, QRegularExpressionValidator\nfrom qgis.PyQt.QtCore import QRegularExpression\n\nself.distanceEdit.setValidator(QDoubleValidator(0.0, 100000.0, 3, self))\n\nself.prefixEdit.setValidator(\n    QRegularExpressionValidator(QRegularExpression(r\"[A-Za-z][A-Za-z0-9_]{0,15}\"), self)\n)\n","python","",[198,230,231,249,262,269,300,305,313,342],{"__ignoreMap":228},[232,233,235,239,243,246],"span",{"class":110,"line":234},1,[232,236,238],{"class":237},"snl16","from",[232,240,242],{"class":241},"s95oV"," qgis.PyQt.QtGui ",[232,244,245],{"class":237},"import",[232,247,248],{"class":241}," QDoubleValidator, QRegularExpressionValidator\n",[232,250,252,254,257,259],{"class":110,"line":251},2,[232,253,238],{"class":237},[232,255,256],{"class":241}," qgis.PyQt.QtCore ",[232,258,245],{"class":237},[232,260,261],{"class":241}," QRegularExpression\n",[232,263,265],{"class":110,"line":264},3,[232,266,268],{"emptyLinePlaceholder":267},true,"\n",[232,270,272,276,279,282,285,288,290,293,295,297],{"class":110,"line":271},4,[232,273,275],{"class":274},"sDLfK","self",[232,277,278],{"class":241},".distanceEdit.setValidator(QDoubleValidator(",[232,280,281],{"class":274},"0.0",[232,283,284],{"class":241},", ",[232,286,287],{"class":274},"100000.0",[232,289,284],{"class":241},[232,291,292],{"class":274},"3",[232,294,284],{"class":241},[232,296,275],{"class":274},[232,298,299],{"class":241},"))\n",[232,301,303],{"class":110,"line":302},5,[232,304,268],{"emptyLinePlaceholder":267},[232,306,308,310],{"class":110,"line":307},6,[232,309,275],{"class":274},[232,311,312],{"class":241},".prefixEdit.setValidator(\n",[232,314,316,319,322,326,329,332,334,337,339],{"class":110,"line":315},7,[232,317,318],{"class":241},"    QRegularExpressionValidator(QRegularExpression(",[232,320,321],{"class":237},"r",[232,323,325],{"class":324},"sU2Wk","\"",[232,327,328],{"class":274},"[A-Za-z][A-Za-z0-9_]",[232,330,331],{"class":237},"{0,15}",[232,333,325],{"class":324},[232,335,336],{"class":241},"), ",[232,338,275],{"class":274},[232,340,341],{"class":241},")\n",[232,343,345],{"class":110,"line":344},8,[232,346,341],{"class":241},[14,348,349,352,353,357,358,361],{},[190,350,351],{},"Breakdown:"," A validator rejects keystrokes that would make the field invalid, so a distance field cannot contain letters and a prefix cannot start with a digit. What it does ",[354,355,356],"em",{},"not"," do is guarantee the final value is acceptable: ",[198,359,360],{},"QDoubleValidator"," allows an intermediate state such as an empty string or a lone minus sign, because the user is still typing. That is correct behaviour and it is why a validator alone is never enough.",[14,363,364,365,368,369,372],{},"Prefer a ",[198,366,367],{},"QgsDoubleSpinBox"," or ",[198,370,371],{},"QgsSpinBox"," over a validated line edit where the input is genuinely numeric — they enforce the range, show the units, and handle locale decimal separators, which a hand-rolled validator does not.",[179,374,376],{"id":375},"layer-two-gate-the-ok-button","Layer two: gate the OK button",[14,378,379],{},"The main work is one method that answers \"is this dialog acceptable, and if not, why\".",[223,381,383],{"className":225,"code":382,"language":227,"meta":228,"style":228},"from qgis.PyQt.QtWidgets import QDialogButtonBox\n\n\ndef problems(self):\n    \"\"\"Return a list of human-readable problems, empty when valid.\"\"\"\n    issues = []\n    layer = self.layerCombo.currentLayer()\n    if layer is None:\n        issues.append(\"Choose an input layer.\")\n    elif layer.featureCount() == 0:\n        issues.append(f\"'{layer.name()}' has no features.\")\n\n    if not self.fieldCombo.currentField():\n        issues.append(\"Choose a field to summarise.\")\n\n    text = self.distanceEdit.text().strip()\n    if not text:\n        issues.append(\"Enter a buffer distance.\")\n    elif float(text) \u003C= 0:\n        issues.append(\"The buffer distance must be greater than zero.\")\n\n    if not self.outputWidget.filePath():\n        issues.append(\"Choose an output file.\")\n\n    return issues\n\n\ndef revalidate(self, *_):\n    issues = self.problems()\n    self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(not issues)\n    self.messageLabel.setText(issues[0] if issues else \"\")\n",[198,384,385,397,401,405,417,422,433,446,463,474,491,516,521,534,544,549,562,572,582,600,610,615,627,637,642,651,656,661,678,690,704],{"__ignoreMap":228},[232,386,387,389,392,394],{"class":110,"line":234},[232,388,238],{"class":237},[232,390,391],{"class":241}," qgis.PyQt.QtWidgets ",[232,393,245],{"class":237},[232,395,396],{"class":241}," QDialogButtonBox\n",[232,398,399],{"class":110,"line":251},[232,400,268],{"emptyLinePlaceholder":267},[232,402,403],{"class":110,"line":264},[232,404,268],{"emptyLinePlaceholder":267},[232,406,407,410,414],{"class":110,"line":271},[232,408,409],{"class":237},"def",[232,411,413],{"class":412},"svObZ"," problems",[232,415,416],{"class":241},"(self):\n",[232,418,419],{"class":110,"line":302},[232,420,421],{"class":324},"    \"\"\"Return a list of human-readable problems, empty when valid.\"\"\"\n",[232,423,424,427,430],{"class":110,"line":307},[232,425,426],{"class":241},"    issues ",[232,428,429],{"class":237},"=",[232,431,432],{"class":241}," []\n",[232,434,435,438,440,443],{"class":110,"line":315},[232,436,437],{"class":241},"    layer ",[232,439,429],{"class":237},[232,441,442],{"class":274}," self",[232,444,445],{"class":241},".layerCombo.currentLayer()\n",[232,447,448,451,454,457,460],{"class":110,"line":344},[232,449,450],{"class":237},"    if",[232,452,453],{"class":241}," layer ",[232,455,456],{"class":237},"is",[232,458,459],{"class":274}," None",[232,461,462],{"class":241},":\n",[232,464,466,469,472],{"class":110,"line":465},9,[232,467,468],{"class":241},"        issues.append(",[232,470,471],{"class":324},"\"Choose an input layer.\"",[232,473,341],{"class":241},[232,475,477,480,483,486,489],{"class":110,"line":476},10,[232,478,479],{"class":237},"    elif",[232,481,482],{"class":241}," layer.featureCount() ",[232,484,485],{"class":237},"==",[232,487,488],{"class":274}," 0",[232,490,462],{"class":241},[232,492,494,496,499,502,505,508,511,514],{"class":110,"line":493},11,[232,495,468],{"class":241},[232,497,498],{"class":237},"f",[232,500,501],{"class":324},"\"'",[232,503,504],{"class":274},"{",[232,506,507],{"class":241},"layer.name()",[232,509,510],{"class":274},"}",[232,512,513],{"class":324},"' has no features.\"",[232,515,341],{"class":241},[232,517,519],{"class":110,"line":518},12,[232,520,268],{"emptyLinePlaceholder":267},[232,522,524,526,529,531],{"class":110,"line":523},13,[232,525,450],{"class":237},[232,527,528],{"class":237}," not",[232,530,442],{"class":274},[232,532,533],{"class":241},".fieldCombo.currentField():\n",[232,535,537,539,542],{"class":110,"line":536},14,[232,538,468],{"class":241},[232,540,541],{"class":324},"\"Choose a field to summarise.\"",[232,543,341],{"class":241},[232,545,547],{"class":110,"line":546},15,[232,548,268],{"emptyLinePlaceholder":267},[232,550,552,555,557,559],{"class":110,"line":551},16,[232,553,554],{"class":241},"    text ",[232,556,429],{"class":237},[232,558,442],{"class":274},[232,560,561],{"class":241},".distanceEdit.text().strip()\n",[232,563,565,567,569],{"class":110,"line":564},17,[232,566,450],{"class":237},[232,568,528],{"class":237},[232,570,571],{"class":241}," text:\n",[232,573,575,577,580],{"class":110,"line":574},18,[232,576,468],{"class":241},[232,578,579],{"class":324},"\"Enter a buffer distance.\"",[232,581,341],{"class":241},[232,583,585,587,590,593,596,598],{"class":110,"line":584},19,[232,586,479],{"class":237},[232,588,589],{"class":274}," float",[232,591,592],{"class":241},"(text) ",[232,594,595],{"class":237},"\u003C=",[232,597,488],{"class":274},[232,599,462],{"class":241},[232,601,603,605,608],{"class":110,"line":602},20,[232,604,468],{"class":241},[232,606,607],{"class":324},"\"The buffer distance must be greater than zero.\"",[232,609,341],{"class":241},[232,611,613],{"class":110,"line":612},21,[232,614,268],{"emptyLinePlaceholder":267},[232,616,618,620,622,624],{"class":110,"line":617},22,[232,619,450],{"class":237},[232,621,528],{"class":237},[232,623,442],{"class":274},[232,625,626],{"class":241},".outputWidget.filePath():\n",[232,628,630,632,635],{"class":110,"line":629},23,[232,631,468],{"class":241},[232,633,634],{"class":324},"\"Choose an output file.\"",[232,636,341],{"class":241},[232,638,640],{"class":110,"line":639},24,[232,641,268],{"emptyLinePlaceholder":267},[232,643,645,648],{"class":110,"line":644},25,[232,646,647],{"class":237},"    return",[232,649,650],{"class":241}," issues\n",[232,652,654],{"class":110,"line":653},26,[232,655,268],{"emptyLinePlaceholder":267},[232,657,659],{"class":110,"line":658},27,[232,660,268],{"emptyLinePlaceholder":267},[232,662,664,666,669,672,675],{"class":110,"line":663},28,[232,665,409],{"class":237},[232,667,668],{"class":412}," revalidate",[232,670,671],{"class":241},"(self, ",[232,673,674],{"class":237},"*",[232,676,677],{"class":241},"_):\n",[232,679,681,683,685,687],{"class":110,"line":680},29,[232,682,426],{"class":241},[232,684,429],{"class":237},[232,686,442],{"class":274},[232,688,689],{"class":241},".problems()\n",[232,691,693,696,699,701],{"class":110,"line":692},30,[232,694,695],{"class":274},"    self",[232,697,698],{"class":241},".buttonBox.button(QDialogButtonBox.Ok).setEnabled(",[232,700,356],{"class":237},[232,702,703],{"class":241}," issues)\n",[232,705,707,709,712,714,717,720,723,726,729],{"class":110,"line":706},31,[232,708,695],{"class":274},[232,710,711],{"class":241},".messageLabel.setText(issues[",[232,713,46],{"class":274},[232,715,716],{"class":241},"] ",[232,718,719],{"class":237},"if",[232,721,722],{"class":241}," issues ",[232,724,725],{"class":237},"else",[232,727,728],{"class":324}," \"\"",[232,730,341],{"class":241},[14,732,733,735,736,739,740,743,744,747,748,751,752,206],{},[190,734,351],{}," Returning a list of sentences rather than a boolean is what makes the same method serve both the button state and the message. Showing only the ",[354,737,738],{},"first"," problem keeps the message short and leads the user through them one at a time, which reads better than a wall of complaints. ",[198,741,742],{},"*_"," on ",[198,745,746],{},"revalidate"," lets it connect to signals with any argument count. Note the ",[198,749,750],{},"float(text)"," is safe only because the validator guarantees the field is numeric — without it this needs a ",[198,753,754],{},"try",[14,756,757],{},"Wire it to every input's change signal, then call it once so the initial state is right:",[223,759,761],{"className":225,"code":760,"language":227,"meta":228,"style":228},"def wire(self):\n    self.layerCombo.layerChanged.connect(self.revalidate)\n    self.fieldCombo.fieldChanged.connect(self.revalidate)\n    self.distanceEdit.textChanged.connect(self.revalidate)\n    self.outputWidget.fileChanged.connect(self.revalidate)\n    self.revalidate()\n",[198,762,763,772,784,795,806,817],{"__ignoreMap":228},[232,764,765,767,770],{"class":110,"line":234},[232,766,409],{"class":237},[232,768,769],{"class":412}," wire",[232,771,416],{"class":241},[232,773,774,776,779,781],{"class":110,"line":251},[232,775,695],{"class":274},[232,777,778],{"class":241},".layerCombo.layerChanged.connect(",[232,780,275],{"class":274},[232,782,783],{"class":241},".revalidate)\n",[232,785,786,788,791,793],{"class":110,"line":264},[232,787,695],{"class":274},[232,789,790],{"class":241},".fieldCombo.fieldChanged.connect(",[232,792,275],{"class":274},[232,794,783],{"class":241},[232,796,797,799,802,804],{"class":110,"line":271},[232,798,695],{"class":274},[232,800,801],{"class":241},".distanceEdit.textChanged.connect(",[232,803,275],{"class":274},[232,805,783],{"class":241},[232,807,808,810,813,815],{"class":110,"line":302},[232,809,695],{"class":274},[232,811,812],{"class":241},".outputWidget.fileChanged.connect(",[232,814,275],{"class":274},[232,816,783],{"class":241},[232,818,819,821],{"class":110,"line":307},[232,820,695],{"class":274},[232,822,823],{"class":241},".revalidate()\n",[14,825,826,828,829,832,833,836],{},[190,827,351],{}," The trailing call matters as much as the connections — signals fire on change, and the dialog opens without any change having happened, so without it the OK button starts enabled on an empty form. Connecting to ",[198,830,831],{},"textChanged"," rather than ",[198,834,835],{},"editingFinished"," gives feedback as the user types, which is the difference between a dialog that feels responsive and one that feels like it is judging you after the fact.",[179,838,840],{"id":839},"layer-three-check-again-on-accept","Layer three: check again on accept",[14,842,843],{},"Conditions can change between the dialog opening and OK being pressed — a layer removed from another panel, a file created by another process.",[223,845,847],{"className":225,"code":846,"language":227,"meta":228,"style":228},"from qgis.PyQt.QtWidgets import QDialog\nfrom qgis.core import QgsProject\nimport os\n\n\ndef accept(self):\n    issues = self.problems()\n\n    layer = self.layerCombo.currentLayer()\n    if layer is not None and QgsProject.instance().mapLayer(layer.id()) is None:\n        issues.append(\"The input layer was removed from the project.\")\n\n    path = self.outputWidget.filePath()\n    if path and os.path.exists(path) and not self.overwriteCheck.isChecked():\n        issues.append(f\"'{os.path.basename(path)}' already exists. Tick overwrite to replace it.\")\n\n    if issues:\n        self.iface.messageBar().pushWarning(\"Cannot continue\", issues[0])\n        self.revalidate()\n        return\n\n    super().accept()\n",[198,848,849,860,872,879,883,887,896,906,910,920,944,953,957,969,991,1011,1015,1022,1041,1047,1052,1056],{"__ignoreMap":228},[232,850,851,853,855,857],{"class":110,"line":234},[232,852,238],{"class":237},[232,854,391],{"class":241},[232,856,245],{"class":237},[232,858,859],{"class":241}," QDialog\n",[232,861,862,864,867,869],{"class":110,"line":251},[232,863,238],{"class":237},[232,865,866],{"class":241}," qgis.core ",[232,868,245],{"class":237},[232,870,871],{"class":241}," QgsProject\n",[232,873,874,876],{"class":110,"line":264},[232,875,245],{"class":237},[232,877,878],{"class":241}," os\n",[232,880,881],{"class":110,"line":271},[232,882,268],{"emptyLinePlaceholder":267},[232,884,885],{"class":110,"line":302},[232,886,268],{"emptyLinePlaceholder":267},[232,888,889,891,894],{"class":110,"line":307},[232,890,409],{"class":237},[232,892,893],{"class":412}," accept",[232,895,416],{"class":241},[232,897,898,900,902,904],{"class":110,"line":315},[232,899,426],{"class":241},[232,901,429],{"class":237},[232,903,442],{"class":274},[232,905,689],{"class":241},[232,907,908],{"class":110,"line":344},[232,909,268],{"emptyLinePlaceholder":267},[232,911,912,914,916,918],{"class":110,"line":465},[232,913,437],{"class":241},[232,915,429],{"class":237},[232,917,442],{"class":274},[232,919,445],{"class":241},[232,921,922,924,926,928,930,932,935,938,940,942],{"class":110,"line":476},[232,923,450],{"class":237},[232,925,453],{"class":241},[232,927,456],{"class":237},[232,929,528],{"class":237},[232,931,459],{"class":274},[232,933,934],{"class":237}," and",[232,936,937],{"class":241}," QgsProject.instance().mapLayer(layer.id()) ",[232,939,456],{"class":237},[232,941,459],{"class":274},[232,943,462],{"class":241},[232,945,946,948,951],{"class":110,"line":493},[232,947,468],{"class":241},[232,949,950],{"class":324},"\"The input layer was removed from the project.\"",[232,952,341],{"class":241},[232,954,955],{"class":110,"line":518},[232,956,268],{"emptyLinePlaceholder":267},[232,958,959,962,964,966],{"class":110,"line":523},[232,960,961],{"class":241},"    path ",[232,963,429],{"class":237},[232,965,442],{"class":274},[232,967,968],{"class":241},".outputWidget.filePath()\n",[232,970,971,973,976,979,982,984,986,988],{"class":110,"line":536},[232,972,450],{"class":237},[232,974,975],{"class":241}," path ",[232,977,978],{"class":237},"and",[232,980,981],{"class":241}," os.path.exists(path) ",[232,983,978],{"class":237},[232,985,528],{"class":237},[232,987,442],{"class":274},[232,989,990],{"class":241},".overwriteCheck.isChecked():\n",[232,992,993,995,997,999,1001,1004,1006,1009],{"class":110,"line":546},[232,994,468],{"class":241},[232,996,498],{"class":237},[232,998,501],{"class":324},[232,1000,504],{"class":274},[232,1002,1003],{"class":241},"os.path.basename(path)",[232,1005,510],{"class":274},[232,1007,1008],{"class":324},"' already exists. Tick overwrite to replace it.\"",[232,1010,341],{"class":241},[232,1012,1013],{"class":110,"line":551},[232,1014,268],{"emptyLinePlaceholder":267},[232,1016,1017,1019],{"class":110,"line":564},[232,1018,450],{"class":237},[232,1020,1021],{"class":241}," issues:\n",[232,1023,1024,1027,1030,1033,1036,1038],{"class":110,"line":574},[232,1025,1026],{"class":274},"        self",[232,1028,1029],{"class":241},".iface.messageBar().pushWarning(",[232,1031,1032],{"class":324},"\"Cannot continue\"",[232,1034,1035],{"class":241},", issues[",[232,1037,46],{"class":274},[232,1039,1040],{"class":241},"])\n",[232,1042,1043,1045],{"class":110,"line":584},[232,1044,1026],{"class":274},[232,1046,823],{"class":241},[232,1048,1049],{"class":110,"line":602},[232,1050,1051],{"class":237},"        return\n",[232,1053,1054],{"class":110,"line":612},[232,1055,268],{"emptyLinePlaceholder":267},[232,1057,1058,1061],{"class":110,"line":617},[232,1059,1060],{"class":274},"    super",[232,1062,1063],{"class":241},"().accept()\n",[14,1065,1066,1068,1069,1072,1073,1076,1077,1080],{},[190,1067,351],{}," Overriding ",[198,1070,1071],{},"accept()"," rather than connecting to the OK button's ",[198,1074,1075],{},"clicked"," signal is the correct hook, because it also covers the user pressing Return. Not calling ",[198,1078,1079],{},"super().accept()"," is what keeps the dialog open — returning early leaves everything as it was, with the message explaining why. The existence check is the one that matters most in practice: silently overwriting somebody's output is a bug they will only notice much later.",[14,1082,1083],{},[29,1084,1087,1090,1093,1096,1099,1103,1107,1115,1119,1123,1127,1131,1135,1138,1141,1145,1149,1152,1155,1158,1161,1165,1169,1175,1178,1182,1186,1190,1194],{"viewBox":1085,"role":32,"ariaLabel":1086,"xmlns":34},"0 0 760 296","Three places a validation message can appear, ranked by how likely the user is to read it: next to the field, in the QGIS message bar, or in a modal dialog",[36,1088,1089],{},"Where the message goes decides whether it is read",[40,1091,1092],{},"A message next to the field the user is looking at is read immediately. A message in the QGIS message bar is seen but requires a glance away from the dialog. A modal message box interrupts and is read, but is disruptive when the problem is a field the user has not filled in yet.",[44,1094],{"x":46,"y":46,"width":47,"height":1095,"fill":49},"296",[68,1097,1098],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Put the message where the eyes already are",[44,1100],{"x":78,"y":1101,"width":80,"height":1102,"rx":82,"fill":119,"stroke":120,"style":85},"56","200",[68,1104,1106],{"x":88,"y":1105,"style":90,"fill":120,"textAnchor":74},"84","next to the field",[44,1108],{"x":1109,"y":1110,"width":1111,"height":1112,"rx":1113,"fill":167,"stroke":107,"style":1114},"44","102","180","26","4","stroke-width:1.4",[68,1116,1118],{"x":88,"y":164,"style":1117,"fill":107,"textAnchor":74},"text-anchor:middle;font-size:9.5px;font-family:sans-serif","distance: (empty)",[68,1120,1122],{"x":88,"y":1121,"style":1117,"fill":168,"textAnchor":74},"150","Enter a buffer distance.",[68,1124,1126],{"x":88,"y":1125,"style":95,"fill":66,"textAnchor":74},"186","read immediately",[68,1128,1130],{"x":88,"y":1129,"style":95,"fill":66,"textAnchor":74},"208","no interruption",[68,1132,1134],{"x":88,"y":1133,"style":95,"fill":120,"textAnchor":74},"234","use for live validation",[44,1136],{"x":1137,"y":1101,"width":80,"height":1102,"rx":82,"fill":83,"stroke":84,"style":85},"266",[68,1139,1140],{"x":70,"y":1105,"style":90,"fill":84,"textAnchor":74},"the message bar",[44,1142],{"x":1143,"y":1110,"width":1111,"height":1112,"rx":1113,"fill":1144,"stroke":84,"style":1114},"290","#dbeafe",[68,1146,1148],{"x":70,"y":164,"style":1147,"fill":66,"textAnchor":74},"text-anchor:middle;font-size:9px;font-family:sans-serif","⚠ Cannot continue",[68,1150,1151],{"x":70,"y":1121,"style":1117,"fill":66,"textAnchor":74},"above the canvas",[68,1153,1154],{"x":70,"y":1125,"style":95,"fill":66,"textAnchor":74},"seen, but a glance away",[68,1156,1157],{"x":70,"y":1129,"style":95,"fill":66,"textAnchor":74},"dismisses itself",[68,1159,1160],{"x":70,"y":1133,"style":95,"fill":84,"textAnchor":74},"use for results and warnings",[44,1162],{"x":1163,"y":1101,"width":1164,"height":1102,"rx":82,"fill":144,"stroke":145,"style":85},"512","224",[68,1166,1168],{"x":1167,"y":1105,"style":90,"fill":145,"textAnchor":74},"624","a modal box",[44,1170],{"x":1171,"y":1110,"width":1172,"height":1173,"rx":1113,"fill":167,"stroke":145,"style":1174},"536","176","40","stroke-width:1.6",[68,1176,1177],{"x":1167,"y":164,"style":1147,"fill":66,"textAnchor":74},"Overwrite existing file?",[68,1179,1181],{"x":1167,"y":1180,"style":1147,"fill":66,"textAnchor":74},"135","Yes · No",[68,1183,1185],{"x":1167,"y":1184,"style":95,"fill":66,"textAnchor":74},"168","always read",[68,1187,1189],{"x":1167,"y":1188,"style":95,"fill":66,"textAnchor":74},"190","always interrupts",[68,1191,1193],{"x":1167,"y":1192,"style":95,"fill":145,"textAnchor":74},"222","use only for decisions",[68,1195,1197],{"x":1167,"y":1196,"style":95,"fill":145,"textAnchor":74},"242","that need an answer",[179,1199,1201],{"id":1200},"a-message-bar-inside-the-dialog","A message bar inside the dialog",[14,1203,1204],{},"The QGIS message bar can be embedded in a dialog, which puts warnings inside the window the user is looking at rather than behind it.",[223,1206,1208],{"className":225,"code":1207,"language":227,"meta":228,"style":228},"from qgis.gui import QgsMessageBar\nfrom qgis.core import Qgis\n\n\ndef setup_message_bar(self):\n    self.bar = QgsMessageBar(self)\n    self.bar.setSizePolicy(\n        QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed\n    )\n    self.layout().insertWidget(0, self.bar)\n\n\ndef warn(self, text):\n    self.bar.clearWidgets()\n    self.bar.pushMessage(\"\", text, level=Qgis.Warning, duration=6)\n",[198,1209,1210,1222,1233,1237,1241,1250,1266,1273,1278,1283,1299,1303,1307,1317,1324],{"__ignoreMap":228},[232,1211,1212,1214,1217,1219],{"class":110,"line":234},[232,1213,238],{"class":237},[232,1215,1216],{"class":241}," qgis.gui ",[232,1218,245],{"class":237},[232,1220,1221],{"class":241}," QgsMessageBar\n",[232,1223,1224,1226,1228,1230],{"class":110,"line":251},[232,1225,238],{"class":237},[232,1227,866],{"class":241},[232,1229,245],{"class":237},[232,1231,1232],{"class":241}," Qgis\n",[232,1234,1235],{"class":110,"line":264},[232,1236,268],{"emptyLinePlaceholder":267},[232,1238,1239],{"class":110,"line":271},[232,1240,268],{"emptyLinePlaceholder":267},[232,1242,1243,1245,1248],{"class":110,"line":302},[232,1244,409],{"class":237},[232,1246,1247],{"class":412}," setup_message_bar",[232,1249,416],{"class":241},[232,1251,1252,1254,1257,1259,1262,1264],{"class":110,"line":307},[232,1253,695],{"class":274},[232,1255,1256],{"class":241},".bar ",[232,1258,429],{"class":237},[232,1260,1261],{"class":241}," QgsMessageBar(",[232,1263,275],{"class":274},[232,1265,341],{"class":241},[232,1267,1268,1270],{"class":110,"line":315},[232,1269,695],{"class":274},[232,1271,1272],{"class":241},".bar.setSizePolicy(\n",[232,1274,1275],{"class":110,"line":344},[232,1276,1277],{"class":241},"        QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed\n",[232,1279,1280],{"class":110,"line":465},[232,1281,1282],{"class":241},"    )\n",[232,1284,1285,1287,1290,1292,1294,1296],{"class":110,"line":476},[232,1286,695],{"class":274},[232,1288,1289],{"class":241},".layout().insertWidget(",[232,1291,46],{"class":274},[232,1293,284],{"class":241},[232,1295,275],{"class":274},[232,1297,1298],{"class":241},".bar)\n",[232,1300,1301],{"class":110,"line":493},[232,1302,268],{"emptyLinePlaceholder":267},[232,1304,1305],{"class":110,"line":518},[232,1306,268],{"emptyLinePlaceholder":267},[232,1308,1309,1311,1314],{"class":110,"line":523},[232,1310,409],{"class":237},[232,1312,1313],{"class":412}," warn",[232,1315,1316],{"class":241},"(self, text):\n",[232,1318,1319,1321],{"class":110,"line":536},[232,1320,695],{"class":274},[232,1322,1323],{"class":241},".bar.clearWidgets()\n",[232,1325,1326,1328,1331,1334,1337,1341,1343,1346,1349,1351,1354],{"class":110,"line":546},[232,1327,695],{"class":274},[232,1329,1330],{"class":241},".bar.pushMessage(",[232,1332,1333],{"class":324},"\"\"",[232,1335,1336],{"class":241},", text, ",[232,1338,1340],{"class":1339},"s9osk","level",[232,1342,429],{"class":237},[232,1344,1345],{"class":241},"Qgis.Warning, ",[232,1347,1348],{"class":1339},"duration",[232,1350,429],{"class":237},[232,1352,1353],{"class":274},"6",[232,1355,341],{"class":241},[14,1357,1358,1360,1361,1364,1365,1368],{},[190,1359,351],{}," Inserting at position 0 puts the bar at the top of the dialog's layout, matching where QGIS puts its own. ",[198,1362,1363],{},"clearWidgets()"," before pushing prevents a stack of messages accumulating when the user tries three times. The empty title argument is deliberate — inside a dialog, the context is already obvious and a title just takes a line. A duration of a few seconds is right for information; use ",[198,1366,1367],{},"duration=0"," for something the user must act on, and clear it yourself when they do.",[179,1370,1372],{"id":1371},"validate-the-output-path-properly","Validate the output path properly",[14,1374,1375],{},"Output paths cause a disproportionate share of late failures, and three checks cover almost all of them.",[223,1377,1379],{"className":225,"code":1378,"language":227,"meta":228,"style":228},"import os\n\n\ndef output_problems(self, path):\n    if not path:\n        return \"Choose an output file.\"\n    directory = os.path.dirname(path) or \".\"\n    if not os.path.isdir(directory):\n        return f\"The folder '{directory}' does not exist.\"\n    if not os.access(directory, os.W_OK):\n        return f\"No permission to write into '{directory}'.\"\n    if os.path.exists(path) and not os.access(path, os.W_OK):\n        return f\"'{os.path.basename(path)}' exists and is not writable.\"\n    return None\n",[198,1380,1381,1387,1391,1395,1405,1414,1422,1438,1447,1467,1482,1500,1517,1534],{"__ignoreMap":228},[232,1382,1383,1385],{"class":110,"line":234},[232,1384,245],{"class":237},[232,1386,878],{"class":241},[232,1388,1389],{"class":110,"line":251},[232,1390,268],{"emptyLinePlaceholder":267},[232,1392,1393],{"class":110,"line":264},[232,1394,268],{"emptyLinePlaceholder":267},[232,1396,1397,1399,1402],{"class":110,"line":271},[232,1398,409],{"class":237},[232,1400,1401],{"class":412}," output_problems",[232,1403,1404],{"class":241},"(self, path):\n",[232,1406,1407,1409,1411],{"class":110,"line":302},[232,1408,450],{"class":237},[232,1410,528],{"class":237},[232,1412,1413],{"class":241}," path:\n",[232,1415,1416,1419],{"class":110,"line":307},[232,1417,1418],{"class":237},"        return",[232,1420,1421],{"class":324}," \"Choose an output file.\"\n",[232,1423,1424,1427,1429,1432,1435],{"class":110,"line":315},[232,1425,1426],{"class":241},"    directory ",[232,1428,429],{"class":237},[232,1430,1431],{"class":241}," os.path.dirname(path) ",[232,1433,1434],{"class":237},"or",[232,1436,1437],{"class":324}," \".\"\n",[232,1439,1440,1442,1444],{"class":110,"line":344},[232,1441,450],{"class":237},[232,1443,528],{"class":237},[232,1445,1446],{"class":241}," os.path.isdir(directory):\n",[232,1448,1449,1451,1454,1457,1459,1462,1464],{"class":110,"line":465},[232,1450,1418],{"class":237},[232,1452,1453],{"class":237}," f",[232,1455,1456],{"class":324},"\"The folder '",[232,1458,504],{"class":274},[232,1460,1461],{"class":241},"directory",[232,1463,510],{"class":274},[232,1465,1466],{"class":324},"' does not exist.\"\n",[232,1468,1469,1471,1473,1476,1479],{"class":110,"line":476},[232,1470,450],{"class":237},[232,1472,528],{"class":237},[232,1474,1475],{"class":241}," os.access(directory, os.",[232,1477,1478],{"class":274},"W_OK",[232,1480,1481],{"class":241},"):\n",[232,1483,1484,1486,1488,1491,1493,1495,1497],{"class":110,"line":493},[232,1485,1418],{"class":237},[232,1487,1453],{"class":237},[232,1489,1490],{"class":324},"\"No permission to write into '",[232,1492,504],{"class":274},[232,1494,1461],{"class":241},[232,1496,510],{"class":274},[232,1498,1499],{"class":324},"'.\"\n",[232,1501,1502,1504,1506,1508,1510,1513,1515],{"class":110,"line":518},[232,1503,450],{"class":237},[232,1505,981],{"class":241},[232,1507,978],{"class":237},[232,1509,528],{"class":237},[232,1511,1512],{"class":241}," os.access(path, os.",[232,1514,1478],{"class":274},[232,1516,1481],{"class":241},[232,1518,1519,1521,1523,1525,1527,1529,1531],{"class":110,"line":523},[232,1520,1418],{"class":237},[232,1522,1453],{"class":237},[232,1524,501],{"class":324},[232,1526,504],{"class":274},[232,1528,1003],{"class":241},[232,1530,510],{"class":274},[232,1532,1533],{"class":324},"' exists and is not writable.\"\n",[232,1535,1536,1538],{"class":110,"line":536},[232,1537,647],{"class":237},[232,1539,1540],{"class":274}," None\n",[14,1542,1543,1545,1546,1548,1549,1552,1553,1556,1557,1560],{},[190,1544,351],{}," Checking the ",[354,1547,1461],{}," rather than the file is what catches the case that matters: a path into a folder that does not exist fails at write time, minutes into a job. ",[198,1550,1551],{},"os.access"," is advisory on some systems and can be wrong on network shares, so it is a good check and not a guarantee — the write itself must still be error-handled. Returning ",[198,1554,1555],{},"None"," for success and a sentence for failure keeps this composable with the ",[198,1558,1559],{},"problems()"," list above.",[179,1562,1564],{"id":1563},"qgis-version-compatibility","QGIS version compatibility",[14,1566,1567,1568,1570],{},"The examples target ",[190,1569,192],{}," (Python 3.12).",[1572,1573,1574,1590],"table",{},[1575,1576,1577],"thead",{},[1578,1579,1580,1584,1587],"tr",{},[1581,1582,1583],"th",{},"QGIS version",[1581,1585,1586],{},"Python",[1581,1588,1589],{},"Notes",[1591,1592,1593,1612,1629,1642,1653],"tbody",{},[1578,1594,1595,1599,1602],{},[1596,1597,1598],"td",{},"3.16 LTR",[1596,1600,1601],{},"3.7",[1596,1603,1604,1607,1608,1611],{},[198,1605,1606],{},"QgsMessageBar"," embeddable in dialogs; ",[198,1609,1610],{},"Qgis.Warning"," level constants present.",[1578,1613,1614,1617,1620],{},[1596,1615,1616],{},"3.22 LTR",[1596,1618,1619],{},"3.9",[1596,1621,1622,1625,1626,206],{},[198,1623,1624],{},"QRegularExpressionValidator"," preferred over the deprecated ",[198,1627,1628],{},"QRegExpValidator",[1578,1630,1631,1634,1636],{},[1596,1632,1633],{},"3.28 LTR",[1596,1635,1619],{},[1596,1637,1638,1641],{},[198,1639,1640],{},"QgsFileWidget.fileChanged"," signal stable.",[1578,1643,1644,1647,1650],{},[1596,1645,1646],{},"3.34 LTR",[1596,1648,1649],{},"3.12",[1596,1651,1652],{},"Baseline for this page.",[1578,1654,1655,1658,1660],{},[1596,1656,1657],{},"3.40+",[1596,1659,1649],{},[1596,1661,1662],{},"Message bar levels gain a scoped enum form; flat names remain.",[179,1664,1666],{"id":1665},"troubleshooting","Troubleshooting",[184,1668,1669,1679,1688,1701,1711,1719],{},[187,1670,1671,1674,1675,1678],{},[190,1672,1673],{},"The OK button starts enabled on an empty form."," ",[198,1676,1677],{},"revalidate()"," was not called once after wiring the signals.",[187,1680,1681,1684,1685,206],{},[190,1682,1683],{},"Validation runs but the button never disables."," The button box was found by the wrong role. Use ",[198,1686,1687],{},"QDialogButtonBox.Ok",[187,1689,1690,1674,1693,1695,1696,1698,1699,206],{},[190,1691,1692],{},"The dialog closes despite the check.",[198,1694,1079],{}," is being called unconditionally, or the check is on ",[198,1697,1075],{}," rather than in ",[198,1700,1071],{},[187,1702,1703,1708,1709,206],{},[190,1704,1705,1707],{},[198,1706,750],{}," raises."," The field has no validator, or the validator allows an intermediate state. Guard with ",[198,1710,754],{},[187,1712,1713,1674,1716,1718],{},[190,1714,1715],{},"Messages pile up in the bar.",[198,1717,1363],{}," before each push.",[187,1720,1721,1674,1724,1726],{},[190,1722,1723],{},"The output write still fails.",[198,1725,1551],{}," is advisory. Handle the write error as well as checking beforehand.",[179,1728,1730],{"id":1729},"conclusion","Conclusion",[14,1732,1733,1734,1736,1737,1739],{},"Put a validator on the widget where the input has a shape, gate the OK button from one ",[198,1735,1559],{}," method that returns sentences, and re-check on ",[198,1738,1071],{}," for anything that could have changed since. Show the first problem next to the fields, keep a message bar in the dialog for warnings, and check the output directory rather than the output file.",[179,1741,1743],{"id":1742},"frequently-asked-questions","Frequently Asked Questions",[14,1745,1746,1749],{},[190,1747,1748],{},"Should I disable the OK button or show an error on click?","\nDisable it, and say why. A disabled button with no explanation is worse than either — the label next to the fields is what makes disabling acceptable.",[14,1751,1752,1755,1758,1759,1762],{},[190,1753,1754],{},"How do I validate an expression field?",[198,1756,1757],{},"QgsExpressionLineEdit"," validates as you type and exposes ",[198,1760,1761],{},"isValidExpression()",". Combine it with a check that the expression's referenced fields exist on the chosen layer, which it cannot know.",[14,1764,1765,1768,1769,206],{},[190,1766,1767],{},"Where should long-running validation go?","\nNowhere in the dialog. If checking validity is expensive — counting features across a network layer, for instance — do the cheap checks live and the expensive one after accept, reporting through the ",[21,1770,1772],{"href":1771},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002F","message bar and a task",[14,1774,1775,1778,1779,1782,1783,206],{},[190,1776,1777],{},"Do translated messages need anything special?","\nWrap each string in ",[198,1780,1781],{},"self.tr()"," so Qt Linguist picks them up — see ",[21,1784,1786],{"href":1785},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Ftranslate-qgis-plugin-with-qt-linguist\u002F","translating a QGIS plugin with Qt Linguist",[179,1788,1790],{"id":1789},"related","Related",[184,1792,1793,1798,1803,1808,1813],{},[187,1794,1795,1797],{},[21,1796,24],{"href":23}," — the guide this recipe belongs to",[187,1799,1800],{},[21,1801,1802],{"href":212},"Use QgsMapLayerComboBox in a Plugin Dialog",[187,1804,1805],{},[21,1806,1807],{"href":204},"Load a .ui File at Runtime in PyQGIS",[187,1809,1810],{},[21,1811,1812],{"href":1771},"Show Plugin Progress and Cancellation in PyQGIS",[187,1814,1815],{},[21,1816,1817],{"href":1785},"Translate a QGIS Plugin with Qt Linguist",[1819,1820,1821],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":228,"searchDepth":251,"depth":251,"links":1823},[1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834],{"id":181,"depth":251,"text":182},{"id":217,"depth":251,"text":218},{"id":375,"depth":251,"text":376},{"id":839,"depth":251,"text":840},{"id":1200,"depth":251,"text":1201},{"id":1371,"depth":251,"text":1372},{"id":1563,"depth":251,"text":1564},{"id":1665,"depth":251,"text":1666},{"id":1729,"depth":251,"text":1730},{"id":1742,"depth":251,"text":1743},{"id":1789,"depth":251,"text":1790},"Stop a QGIS plugin acting on bad input — live validation with QgsMessageBar, Qt validators, disabling the OK button, and reporting problems where the user is looking.","md",{"slug":1838,"type":1839,"breadcrumb":1840,"datePublished":1841,"dateModified":1841},"validate-plugin-dialog-input-pyqgis","article","Validating Dialog Input","2026-08-27","\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fvalidate-plugin-dialog-input-pyqgis",{"title":5,"description":1835},"qgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fvalidate-plugin-dialog-input-pyqgis\u002Findex","PCaHZZXn06Rf8kP30i2hLlHI8LV-39RZjpGrCNL0Ha4",1787823363168]