กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

เมื่อเราเจาะลึกหัวข้อวิธีการทำงานของผลิตภัณฑ์ 1C ในสภาพแวดล้อม Linux มีการค้นพบข้อเสียเปรียบประการหนึ่งนั่นคือการขาดเครื่องมือกราฟิกหลายแพลตฟอร์มที่สะดวกสำหรับการจัดการคลัสเตอร์ของเซิร์ฟเวอร์ 1C และมีการตัดสินใจที่จะแก้ไขข้อเสียเปรียบนี้ด้วยการเขียน GUI สำหรับยูทิลิตี้คอนโซล rac ในความคิดของฉัน Tcl/tk ได้รับเลือกให้เป็นภาษาสำหรับการพัฒนาซึ่งเหมาะสมที่สุดสำหรับงานนี้ ดังนั้นฉันจึงอยากจะนำเสนอแง่มุมที่น่าสนใจของวิธีแก้ปัญหาในเอกสารนี้

ในการทำงาน คุณจะต้องมีการกระจาย tcl/tk และ 1C และเนื่องจากฉันตัดสินใจที่จะใช้ประโยชน์สูงสุดจากความสามารถในการจัดส่ง tcl/tk พื้นฐานโดยไม่ต้องใช้แพ็คเกจของบุคคลที่สาม ฉันจะต้องมีเวอร์ชัน 8.6.7 ซึ่งรวมถึง ttk - แพ็คเกจที่มีองค์ประกอบกราฟิกเพิ่มเติม ซึ่งเราต้องการ ttk เป็นหลัก ::TreeView ช่วยให้สามารถแสดงข้อมูลทั้งในรูปแบบของโครงสร้างต้นไม้และในรูปแบบของตาราง (รายการ) นอกจากนี้ในเวอร์ชันใหม่ การทำงานที่มีข้อยกเว้นยังได้รับการปรับปรุงใหม่ (คำสั่ง try ซึ่งใช้ในโครงการเมื่อรันคำสั่งภายนอก)

โปรเจ็กต์ประกอบด้วยไฟล์หลายไฟล์ (แม้ว่าจะไม่มีสิ่งใดขัดขวางไม่ให้คุณทำทุกอย่างในที่เดียว):

rac_gui.cfg - การกำหนดค่าเริ่มต้น
rac_gui.tcl - สคริปต์เริ่มต้นหลัก
ไดเร็กทอรี lib ประกอบด้วยไฟล์ที่โหลดโดยอัตโนมัติเมื่อเริ่มต้นระบบ:
function.tcl - ไฟล์พร้อมขั้นตอน
gui.tcl - ส่วนต่อประสานกราฟิกหลัก
images.tcl - ไลบรารีรูปภาพ base64

ที่จริงแล้วไฟล์ rac_gui.tcl จะเริ่มการทำงานของล่าม เริ่มต้นตัวแปร โหลดโมดูล การกำหนดค่า และอื่นๆ เนื้อหาของไฟล์พร้อมความคิดเห็น:

rac_gui.tcl

#!/bin/sh
exec wish "$0" -- "$@"

# Устанавливаем текущий каталог
set dir(root) [pwd]
# Устанавливаем рабочий каталог, если его нет то создаём
set dir(work) [file join $env(HOME) .rac_gui]
if {[file exists $dir(work)] == 0 } {
    file mkdir $dir(work)    
}
# каталог с модулями
set dir(lib) "[file join $dir(root) lib]"

# загружаем пользовательский конфиг, если он отсутствует, то копируем дефолтный
if {[file exists [file join $dir(work) rac_gui.cfg]] ==0} {
    file copy [file join [pwd] rac_gui.cfg] [file join $dir(work) rac_gui.cfg]
} 
source [file join $dir(work) rac_gui.cfg]
# Код проверки наличия rac и правильности указания пути в конфиге
# если программа не найдена то будет выведен диалог для указания корректного пути
# и этот путь будет записан в пользовательский конфиг
if {[file exists $rac_cmd] == 0} {
    set rac_cmd [tk_getOpenFile -initialdir $env(HOME) -parent . -title "Укажите путь до rac" -initialfile rac]
    file copy [file join $dir(work) rac_gui.cfg] [file join $dir(work) rac_gui.cfg.bak] 
    set orig_file [open [file join $dir(work) rac_gui.cfg.bak] "r"]
    set file [open [file join $dir(work) rac_gui.cfg] "w"]
    while {[gets $orig_file line] >=0 } {
        if {[string match "set rac_cmd*" $line]} {
            puts $file "set rac_cmd $rac_cmd"
        } else {
            puts $file $line
        }
    }
    close $file
    close $orig_file
    #return "$host:$port"
    file delete [file join $dir(work) 1c_srv.cfg.bak] 
} else {
    puts "Found $rac_cmd"
}

set cluster_user ""
set cluster_pwd ""
set agent_user ""
set agent_pwd ""
## LOAD FILE ##
# Загружаем модули кроме gui.tcl так как его надо загрузить последним
foreach modFile [lsort [glob -nocomplain [file join $dir(lib) *.tcl]]] {
    if {[file tail $modFile] ne "gui.tcl"} {
        source $modFile
        puts "Loaded module $modFile"
    }
}
source [file join $dir(lib) gui.tcl]
source [file join $dir(work) rac_gui.cfg]

# Читаем файл со списком серверов 1С
# и добавляем в дерево
if [file exists [file join $dir(work) 1c_srv.cfg]] {
    set f [open [file join $dir(work) 1c_srv.cfg] "RDONLY"]
    while {[gets $f line] >=0} {
        .frm_tree.tree insert {} end -id "server::$line" -text "$line" -values "$line"
    }    
}

หลังจากดาวน์โหลดทุกสิ่งที่จำเป็นและตรวจสอบว่ามียูทิลิตี้ rac หรือไม่ หน้าต่างกราฟิกจะเปิดขึ้น อินเทอร์เฟซของโปรแกรมประกอบด้วยสามองค์ประกอบ:

แถบเครื่องมือ แผนผัง และรายการ

ฉันสร้างเนื้อหาของ "ต้นไม้" ให้คล้ายกับอุปกรณ์ Windows มาตรฐานจาก 1C มากที่สุด

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

รหัสหลักที่สร้างหน้าต่างนี้มีอยู่ในไฟล์
lib/gui.tcl

# установка размера и положения основного окна
# можно установить в переменную topLevelGeometry в конфиг программы
if {[info exists topLevelGeometry]} {
    wm geometry . $topLevelGeometry
} else {
    wm geometry . 1024x768
}
# Заголовок окна
wm title . "1C Rac GUI"
wm iconname . "1C Rac Gui"
# иконка окна (берется из файла lib/imges.tcl)
wm iconphoto . tcl
wm protocol . WM_DELETE_WINDOW Quit
wm overrideredirect . 0
wm positionfrom . user

ttk::style theme use clam

# Панель инсрументов
set frm_tool [frame .frm_tool]
pack $frm_tool -side left -fill y 
ttk::panedwindow .panel -orient horizontal -style TPanedwindow
pack .panel -expand true -fill both
pack propagate .panel false

ttk::button $frm_tool.btn_add  -command Add  -image add_grey_32
ttk::button $frm_tool.btn_del  -command Del -image del_grey_32
ttk::button $frm_tool.btn_edit  -command Edit -image edit_grey_32
ttk::button $frm_tool.btn_quit -command Quit -image quit_grey_32

pack $frm_tool.btn_add $frm_tool.btn_del $frm_tool.btn_edit -side top -padx 5 -pady 5
pack $frm_tool.btn_quit  -side bottom -padx 5 -pady 5

# Дерево с полосами прокрутки
set frm_tree [frame .frm_tree]

ttk::scrollbar $frm_tree.hsb1 -orient horizontal -command [list $frm_tree.tree xview]
ttk::scrollbar $frm_tree.vsb1 -orient vertical -command [list $frm_tree.tree yview]
set tree [ttk::treeview $frm_tree.tree -show tree 
-xscrollcommand [list $frm_tree.hsb1 set] -yscrollcommand [list $frm_tree.vsb1 set]]

grid $tree -row 0 -column 0 -sticky nsew
grid $frm_tree.vsb1 -row 0 -column 1 -sticky nsew
grid $frm_tree.hsb1 -row 1 -column 0 -sticky nsew
grid columnconfigure $frm_tree 0 -weight 1
grid rowconfigure $frm_tree 0 -weight 1

# назначение обработчика нажатия кнопкой мыши
bind $frm_tree.tree <ButtonRelease> "TreePress $frm_tree.tree"

# Список для данных (таблица)
set frm_work [frame .frm_work]
ttk::scrollbar $frm_work.hsb -orient horizontal -command [list $frm_work.tree_work xview]
ttk::scrollbar $frm_work.vsb -orient vertical -command [list $frm_work.tree_work yview]
set tree_work [
    ttk::treeview $frm_work.tree_work 
    -show headings  -columns "par val" -displaycolumns "par val"
    -xscrollcommand [list $frm_work.hsb set] 
    -yscrollcommand [list $frm_work.vsb set]
]
# Установка цветов для чередования в таблице
$tree_work tag configure dark -background $color(dark_table_bg)
$tree_work tag configure light -background $color(light_table_bg)

# Размещение элементов на форме
grid $tree_work -row 0 -column 0 -sticky nsew
grid $frm_work.vsb -row 0 -column 1 -sticky nsew
grid $frm_work.hsb -row 1 -column 0 -sticky nsew
grid columnconfigure $frm_work 0 -weight 1
grid rowconfigure $frm_work 0 -weight 1
pack $frm_tree $frm_work -side left -expand true -fill both

#.panel add $frm_tool -weight 1
.panel add $frm_tree -weight 1 
.panel add $frm_work -weight 1

อัลกอริทึมสำหรับการทำงานกับโปรแกรมมีดังนี้:

1. ขั้นแรก คุณต้องเพิ่มเซิร์ฟเวอร์คลัสเตอร์หลัก (เช่น เซิร์ฟเวอร์การจัดการคลัสเตอร์ (ใน Linux การจัดการจะถูกเรียกใช้ด้วยคำสั่ง “/opt/1C/v8.3/x86_64/rasคลัสเตอร์ —daemon”))

โดยคลิกที่ปุ่ม "+" และในหน้าต่างที่เปิดขึ้น ให้ป้อนที่อยู่เซิร์ฟเวอร์และพอร์ต:

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

หลังจากนั้น เซิร์ฟเวอร์ของเราจะปรากฏบนแผนผังโดยการคลิกที่มัน รายการคลัสเตอร์จะเปิดขึ้นหรือข้อผิดพลาดในการเชื่อมต่อจะปรากฏขึ้น

2. การคลิกที่ชื่อคลัสเตอร์จะเป็นการเปิดรายการฟังก์ชันที่พร้อมใช้งาน

3. ...

และอื่นๆ กล่าวคือ หากต้องการเพิ่มคลัสเตอร์ใหม่ ให้เลือกคลัสเตอร์ที่มีอยู่ในรายการแล้วกดปุ่ม "+" ในแถบเครื่องมือ จากนั้นกล่องโต้ตอบเพิ่มใหม่จะปรากฏขึ้น:

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

ปุ่มต่างๆ ในแถบเครื่องมือจะทำหน้าที่ต่างๆ ขึ้นอยู่กับบริบท เช่น ขึ้นอยู่กับองค์ประกอบของต้นไม้หรือรายการที่เลือก จะดำเนินการอย่างใดอย่างหนึ่งหรืออย่างอื่น

ลองดูตัวอย่างของปุ่มเพิ่ม (“+”):

รหัสการสร้างปุ่ม:

ttk::button $frm_tool.btn_add  -command Add  -image add_grey_32

ที่นี่เราจะเห็นว่าเมื่อกดปุ่ม ขั้นตอน "เพิ่ม" จะดำเนินการ รหัสของมัน:

proc Add {} {
    global active_cluster host
    # Определяем идентификатор выделенного элемента
    set id  [.frm_tree.tree selection] 
    # Определяем значение этого элемента
    set values [.frm_tree.tree item [.frm_tree.tree selection] -values]
    set key [lindex [split $id "::"] 0]
    # в зависимости от того что выделили будет запущена нужная процедура
    if {$key eq "" || $key eq "server"} {
        set host [ Add::server ]
        return
    }
    Add::$key .frm_tree.tree $host $values
}

นี่คือข้อดีประการหนึ่งของจี้: คุณสามารถส่งค่าของตัวแปรเป็นชื่อขั้นตอนได้:

Add::$key .frm_tree.tree $host $values

นั่นคือตัวอย่างเช่นหากเราชี้ไปที่เซิร์ฟเวอร์หลักแล้วกด "+" กระบวนการ Add::server จะเปิดตัวหากอยู่ที่คลัสเตอร์ - เพิ่ม :: คลัสเตอร์ และอื่น ๆ (ฉันจะเขียนเกี่ยวกับตำแหน่งที่ “คีย์” ที่จำเป็นมาจากด้านล่างเล็กน้อย) ขั้นตอนที่ระบุไว้จะดึงองค์ประกอบกราฟิกที่เหมาะสมกับบริบท

ดังที่คุณอาจสังเกตเห็นแล้วว่าแบบฟอร์มมีสไตล์คล้ายกัน - ไม่น่าแปลกใจเนื่องจากมีการแสดงโดยขั้นตอนเดียวโดยเฉพาะเฟรมหลักของแบบฟอร์ม (หน้าต่าง, ปุ่ม, รูปภาพ, ป้ายกำกับ) ชื่อของขั้นตอน เพิ่มระดับท็อป

proc AddToplevel {lbl img {win_name .add}} {
    set cmd "destroy $win_name"
    if [winfo exists $win_name] {destroy $win_name}
    toplevel $win_name
    wm title $win_name $lbl
    wm iconphoto $win_name tcl
    # метка с иконкой
    ttk::label $win_name.lbl -image $img
    # фрейм с полями ввода
    set frm [ttk::labelframe $win_name.frm -text $lbl -labelanchor nw]
    
    grid columnconfigure $frm 0 -weight 1
    grid rowconfigure $frm 0 -weight 1
    # фрейм и кнопки
    set frm_btn [frame $win_name.frm_btn -border 0]
    ttk::button $frm_btn.btn_ok -image ok_grey_24 -command { }
    ttk::button $frm_btn.btn_cancel -command $cmd -image quit_grey_24 
    grid $win_name.lbl -row 0 -column 0 -sticky nw -padx 5 -pady 10
    grid $frm -row 0 -column 1 -sticky nw -padx 5 -pady 5
    grid $frm_btn -row 1 -column 1 -sticky se -padx 5 -pady 5
    pack  $frm_btn.btn_cancel  -side right
    pack  $frm_btn.btn_ok  -side right -padx 10
    return $frm
}

พารามิเตอร์การโทร: ชื่อ ชื่อรูปภาพสำหรับไอคอนจากไลบรารี (lib/images.tcl) และพารามิเตอร์ชื่อหน้าต่างเสริม (ค่าเริ่มต้น .add) ดังนั้น หากเราใช้ตัวอย่างข้างต้นในการเพิ่มเซิร์ฟเวอร์หลักและคลัสเตอร์ การเรียกจะเป็นดังนี้:

AddToplevel "Добавление основного сервера" server_grey_64

หรือ

AddToplevel "Добавление кластера" cluster_grey_64

ดำเนินการต่อด้วยตัวอย่างเหล่านี้ ฉันจะแสดงขั้นตอนที่แสดงกล่องโต้ตอบเพิ่มสำหรับเซิร์ฟเวอร์หรือคลัสเตอร์

เพิ่ม::เซิร์ฟเวอร์

proc Add::server {} {
    global default
    # выводим основную форму
    set frm [AddToplevel "Добавление основного сервера" server_grey_64]
    # добавляем етки и поля ввода на эту форму
    label $frm.lbl_host -text "Адрес сервера"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Порт"
    entry $frm.ent_port 
    $frm.ent_port  insert end $default(port)
    grid $frm.lbl_host -row 0 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_host -row 0 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_port -row 1 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_port -row 1 -column 1 -sticky nsew -padx 5 -pady 5
    grid columnconfigure $frm 0 -weight 1
    grid rowconfigure $frm 0 -weight 1
    #set frm_btn [frame .add.frm_btn -border 0]
    # переопределяем обработчик нажатия кнопки
    .add.frm_btn.btn_ok configure -command {
        set host [SaveMainServer [.add.frm.ent_host get] [.add.frm.ent_port get]]
        .frm_tree.tree insert {} end -id "server::$host" -text "$host" -values "$host"
        destroy .add
        return $host
    }
    return $frm
}

เพิ่ม::คลัสเตอร์

proc Add::cluster {tree host values} {
    global default lifetime_limit expiration_timeout session_fault_tolerance_level
    global max_memory_size max_memory_time_limit errors_count_threshold security_level
    global load_balancing_mode kill_problem_processes 
    agent_user agent_pwd cluster_user cluster_pwd auth_agent
    if {$agent_user ne "" && $agent_pwd ne ""} {
        set auth_agent "--agent-user=$agent_user --agent-pwd=$agent_pwd"
    } else {
        set auth_agent ""
    }
    # устанавливаем глобальные переменные ()
    set lifetime_limit $default(lifetime_limit)
    set expiration_timeout $default(expiration_timeout)
    set session_fault_tolerance_level $default(session_fault_tolerance_level)
    set max_memory_size $default(max_memory_size)
    set max_memory_time_limit $default(max_memory_time_limit)
    set errors_count_threshold $default(errors_count_threshold)
    set security_level [lindex $default(security_level) 0]
    set load_balancing_mode [lindex $default(load_balancing_mode) 0]
    
    set frm [AddToplevel "Добавление кластера" cluster_grey_64]
    
    label $frm.lbl_host -text "Адрес основного сервера"
    entry  $frm.ent_host
    label $frm.lbl_port -text "Порт"
    entry $frm.ent_port 
    $frm.ent_port  insert end $default(port)
    label $frm.lbl_name -text "Название кластера"
    entry  $frm.ent_name
    label $frm.lbl_secure_connect -text "Защищённое соединение"
    ttk::combobox $frm.cb_security_level -textvariable security_level -values $default(security_level)
    label $frm.lbl_expiration_timeout -text "Останавливать выключенные процессы через:"
    entry  $frm.ent_expiration_timeout -textvariable expiration_timeout
    label $frm.lbl_session_fault_tolerance_level -text "Уровень отказоустойчивости"
    entry  $frm.ent_session_fault_tolerance_level -textvariable session_fault_tolerance_level
    label $frm.lbl_load_balancing_mode -text "Режим распределения нагрузки"
    ttk::combobox $frm.cb_load_balancing_mode -textvariable load_balancing_mode 
    -values $default(load_balancing_mode)
    label $frm.lbl_errors_count_threshold -text "Допустимое отклонение количества ошибок сервера, %"
    entry  $frm.ent_errors_count_threshold -textvariable errors_count_threshold
    label $frm.lbl_processes -text "Рабочие процессы:"
    label $frm.lbl_lifetime_limit -text "Период перезапуска, сек."
    entry  $frm.ent_lifetime_limit -textvariable lifetime_limit
    label $frm.lbl_max_memory_size -text "Допустимый объём памяти, КБ"
    entry  $frm.ent_max_memory_size -textvariable max_memory_size
    label $frm.lbl_max_memory_time_limit -text "Интервал превышения допустимого объёма памяти, сек."
    entry  $frm.ent_max_memory_time_limit -textvariable max_memory_time_limit
    label $frm.lbl_kill_problem_processes -justify left -anchor nw -text "Принудительно завершать проблемные процессы"
    checkbutton $frm.check_kill_problem_processes -variable kill_problem_processes -onvalue yes -offvalue no    
    
    grid $frm.lbl_host -row 0 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_host -row 0 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_port -row 1 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_port -row 1 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_name -row 2 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_name -row 2 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_secure_connect -row 3 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.cb_security_level -row 3 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_expiration_timeout -row 4 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_expiration_timeout -row 4 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_session_fault_tolerance_level -row 5 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_session_fault_tolerance_level -row 5 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_load_balancing_mode -row 6 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.cb_load_balancing_mode -row 6 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_errors_count_threshold -row 7 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_errors_count_threshold -row 7 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_processes -row 8 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.lbl_lifetime_limit -row 9 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_lifetime_limit -row 9 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_max_memory_size -row 10 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_max_memory_size -row 10 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_max_memory_time_limit -row 11 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.ent_max_memory_time_limit -row 11 -column 1 -sticky nsew -padx 5 -pady 5
    grid $frm.lbl_kill_problem_processes -row 12 -column 0 -sticky nw -padx 5 -pady 5
    grid $frm.check_kill_problem_processes -row 12 -column 1 -sticky nw -padx 5 -pady 5
    # переопределяем обработчик
    .add.frm_btn.btn_ok configure -command {
        RunCommand "" "cluster insert 
        --host=[.add.frm.ent_host get] 
        --port=[.add.frm.ent_port get] 
        --name=[.add.frm.ent_name get] 
        --expiration-timeout=$expiration_timeout 
        --lifetime-limit=$lifetime_limit 
        --max-memory-size=$max_memory_size 
        --max-memory-time-limit=$max_memory_time_limit 
        --security-level=$security_level 
        --session-fault-tolerance-level=$session_fault_tolerance_level 
        --load-balancing-mode=$load_balancing_mode 
        --errors-count-threshold=$errors_count_threshold 
        --kill-problem-processes=$kill_problem_processes 
        $auth_agent $host"
        Run::server $tree $host ""
        destroy .add
    }
    return $frm
}

เมื่อเปรียบเทียบโค้ดของขั้นตอนเหล่านี้ ความแตกต่างจะมองเห็นได้ด้วยตาเปล่า ฉันจะเน้นที่ตัวจัดการปุ่ม "ตกลง" ใน Tk คุณสมบัติขององค์ประกอบกราฟิกสามารถแทนที่ได้ระหว่างการทำงานของโปรแกรมโดยใช้ตัวเลือก กำหนดค่า. ตัวอย่างเช่น คำสั่งเริ่มต้นที่จะแสดงปุ่ม:

ttk::button $frm_btn.btn_ok -image ok_grey_24 -command { }

แต่ในรูปแบบของเรา คำสั่งจะขึ้นอยู่กับฟังก์ชันที่จำเป็น:

  .add.frm_btn.btn_ok configure -command {
        RunCommand "" "cluster insert 
        --host=[.add.frm.ent_host get] 
        --port=[.add.frm.ent_port get] 
        --name=[.add.frm.ent_name get] 
        --expiration-timeout=$expiration_timeout 
        --lifetime-limit=$lifetime_limit 
        --max-memory-size=$max_memory_size 
        --max-memory-time-limit=$max_memory_time_limit 
        --security-level=$security_level 
        --session-fault-tolerance-level=$session_fault_tolerance_level 
        --load-balancing-mode=$load_balancing_mode 
        --errors-count-threshold=$errors_count_threshold 
        --kill-problem-processes=$kill_problem_processes 
        $auth_agent $host"
        Run::server $tree $host ""
        destroy .add
    }

ในตัวอย่างข้างต้น ปุ่ม “อุดตัน” จะเริ่มขั้นตอนการเพิ่มคลัสเตอร์

ที่นี่คุ้มค่าที่จะพูดนอกเรื่องเกี่ยวกับการทำงานกับองค์ประกอบกราฟิกใน Tk - สำหรับองค์ประกอบอินพุตข้อมูลต่างๆ (รายการ, คอมโบบ็อกซ์, ปุ่มตรวจสอบ ฯลฯ ) พารามิเตอร์ถูกนำมาใช้เป็นตัวแปรข้อความ:

entry  $frm.ent_lifetime_limit -textvariable lifetime_limit

ตัวแปรนี้ถูกกำหนดไว้ในเนมสเปซส่วนกลางและมีค่าที่ป้อนในปัจจุบัน เหล่านั้น. เพื่อให้ได้ข้อความที่ป้อนจากฟิลด์ คุณเพียงแค่ต้องอ่านค่าที่สอดคล้องกับตัวแปร (แน่นอนว่าต้องกำหนดไว้เมื่อสร้างองค์ประกอบ)

วิธีที่สองในการดึงข้อความที่ป้อน (สำหรับองค์ประกอบของประเภทรายการ) คือการใช้คำสั่ง get:

.add.frm.ent_name get

ทั้งสองวิธีนี้สามารถดูได้ในโค้ดด้านบน

ในกรณีนี้ การคลิกปุ่มนี้จะเป็นการเปิดโพรซีเดอร์ RunCommand ด้วยบรรทัดคำสั่งที่สร้างขึ้นสำหรับการเพิ่มคลัสเตอร์ในแง่ของ rac:

/opt/1C/v8.3/x86_64/rac cluster insert  --host=localhost  --port=1540  --name=dsdsds  --expiration-timeout=0  --lifetime-limit=0  --max-memory-size=0  --max-memory-time-limit=0  --security-level=0  --session-fault-tolerance-level=0  --load-balancing-mode=performance  --errors-count-threshold=0  --kill-problem-processes=no   localhost:1545

ตอนนี้เรามาถึงคำสั่งหลักซึ่งควบคุมการเรียกใช้ rac ด้วยพารามิเตอร์ที่เราต้องการ นอกจากนี้ยังแยกวิเคราะห์เอาต์พุตของคำสั่งเป็นรายการและส่งกลับหากจำเป็น:

เรียกใช้คำสั่ง

proc RunCommand {root par} {
    global dir rac_cmd cluster work_list_row_count agent_user agent_pwd cluster_user cluster_pwd
    puts "$rac_cmd $par"
    set work_list_row_count 0
    # открываем канал в неблокирующем режиме
    # $rac - команда с полным путём
    # $par - сформированные ключи запуска и опции    
    set pipe [open "|$rac_cmd $par" "r"]
    try {
        set lst ""
        set l ""
        # вывод команды добавляем в список списков
        while {[gets $pipe line]>=0} {
            #puts $line
            if {$line eq ""} {
                lappend l $lst
                set lst ""
            } else {
                lappend lst [string trim $line]
            }
        }
        close $pipe
        return $l
    } on error {result options} {
        # Запуск обработчика ошибок
        ErrorParcing $result $options
        return ""
    }
}

หลังจากป้อนข้อมูลเซิร์ฟเวอร์หลักแล้ว มันจะถูกเพิ่มลงในแผนผัง สำหรับสิ่งนี้ ในขั้นตอน Add:server ข้างต้น รหัสต่อไปนี้จะรับผิดชอบ:

.frm_tree.tree insert {} end -id "server::$host" -text "$host" -values "$host"

ตอนนี้ โดยการคลิกที่ชื่อเซิร์ฟเวอร์ในแผนผัง เราจะได้รายการคลัสเตอร์ที่จัดการโดยเซิร์ฟเวอร์นั้น และโดยการคลิกที่คลัสเตอร์ เราก็จะได้รับรายการองค์ประกอบคลัสเตอร์ (เซิร์ฟเวอร์ ฐานข้อมูล ฯลฯ) สิ่งนี้ถูกนำไปใช้ในขั้นตอน TreePress (ไฟล์ lib/function.tcl):

proc TreePress {tree} {
   global host server active_cluster infobase
   # определяем выделенный элемент
    set id  [$tree selection]
   # устанавливаем нужные глобальные переменные
    SetGlobalVarFromTreeItems $tree $id
   # Определяем ключ и значение, т.е. именно тип выбранного элемента
    set values [$tree item $id -values]
    set key [lindex [split $id "::"] 0]
   # и в зависимости от того что выбрали будет запущена соответствующая процедура 
   # в пространстве имён Run
    Run::$key $tree $host $values
}

ดังนั้น Run::server จะถูกเปิดใช้งานสำหรับเซิร์ฟเวอร์หลัก (สำหรับคลัสเตอร์ - Run::cluster สำหรับเซิร์ฟเวอร์ที่ทำงาน - Run::work_server ฯลฯ) เหล่านั้น. ค่าของตัวแปร $key เป็นส่วนหนึ่งของชื่อขององค์ประกอบต้นไม้ที่ระบุโดยตัวเลือก -NS.

มาดูขั้นตอนกันดีกว่า

เรียกใช้::เซิร์ฟเวอร์

proc Run::server {tree host values} {
    # получаем список кластеров требуемого сервера
    set lst [RunCommand server::$host "cluster list $host"]
    if {$lst eq ""} {return}
    set l [lindex $lst 0]
    #puts $lst
    # удаляем лишнее из списка
    .frm_work.tree_work delete  [ .frm_work.tree_work children {}]
    # читаем список
    foreach cluster_list $lst {
        # Заполняем список полученными значениями
        InsertItemsWorkList $cluster_list
        # обрабатываем вывод (список) для добавления данных в дерево
        foreach i $cluster_list {
            #puts $i
            set cluster_list [split $i ":"]
            if  {[string trim [lindex $cluster_list 0]] eq "cluster"} {
                set cluster_id [string trim [lindex $cluster_list 1]]
                lappend cluster($cluster_id) $cluster_id
            }
            if  {[string trim [lindex $cluster_list 0]] eq "name"} {
                lappend  cluster($cluster_id) [string trim [lindex $cluster_list 1]]
            }
        }
    }
    # добавляем кластеры в дерево
    foreach x [array names cluster] {
        set id [lindex $cluster($x) 0]
        if { [$tree exists "cluster::$id"] == 0 } {
            $tree insert "server::$host" end -id "cluster::$id" -text "[lindex $cluster($x) 1]" -values "$id"
            # добавляем элементы в кластер
            InsertClusterItems $tree $id
        }
    }
    if { [$tree exists "agent_admins::$id"] == 0 } {
        $tree insert "server::$host" end -id "agent_admins::$id" -text "Администраторы" -values "$id"
        #InsertClusterItems $tree $id
    }
}

ขั้นตอนนี้ประมวลผลสิ่งที่ได้รับจากเซิร์ฟเวอร์ผ่านคำสั่ง RunCommand และเพิ่มสิ่งต่าง ๆ ทุกประเภทลงในแผนผัง - คลัสเตอร์ องค์ประกอบรูทต่างๆ (ฐาน เซิร์ฟเวอร์ที่ทำงาน เซสชัน และอื่นๆ) หากคุณมองอย่างใกล้ชิด คุณจะสังเกตเห็นการเรียกไปยังกระบวนงาน InsertItemsWorkList ภายใน ใช้เพื่อเพิ่มองค์ประกอบลงในรายการแบบกราฟิกโดยการประมวลผลเอาต์พุตของยูทิลิตี้คอนโซล rac ซึ่งก่อนหน้านี้ส่งคืนเป็นรายการไปยังตัวแปร $lst นี่คือรายการของรายการที่มีคู่ขององค์ประกอบคั่นด้วยเครื่องหมายทวิภาค

ตัวอย่างเช่น รายการการเชื่อมต่อคลัสเตอร์:

svk@svk ~]$ /opt/1C/v8.3/x86_64/rac connection list --cluster=783d2170-56c3-11e8-c586-fc75165efbb2 localhost:1545
connection     : dcf5991c-7d24-11e8-1690-fc75165efbb2
conn-id        : 0
host           : svk.home
process        : 79de2e16-56c3-11e8-c586-fc75165efbb2
infobase       : 00000000-0000-0000-0000-000000000000
application    : "JobScheduler"
connected-at   : 2018-07-01T14:49:51
session-number : 0
blocked-by-ls  : 0

connection     : b993293a-7d24-11e8-1690-fc75165efbb2
conn-id        : 0
host           : svk.home
process        : 79de2e16-56c3-11e8-c586-fc75165efbb2
infobase       : 00000000-0000-0000-0000-000000000000
application    : "JobScheduler"
connected-at   : 2018-07-01T14:48:52
session-number : 0
blocked-by-ls  : 0

ในรูปแบบกราฟิกจะมีลักษณะดังนี้:

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

ขั้นตอนข้างต้นเลือกชื่อขององค์ประกอบสำหรับส่วนหัวและข้อมูลเพื่อกรอกตาราง:

แทรกItemsWorkList

proc InsertItemsWorkList {lst} {
    global work_list_row_count
    # установка чередования цвета для строки
    if [expr $work_list_row_count % 2] {
        set tag dark
    } else {
        set tag light
    }
    # разбор строк на пары ключ - значение
    foreach i $lst {
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            lappend column_list [string trim $param]
            lappend value_list [string trim $value]
        }
    }
     # заполнение таблицы
    .frm_work.tree_work configure -columns $column_list -displaycolumns $column_list
    .frm_work.tree_work insert {} end  -values $value_list -tags $tag
    .frm_work.tree_work column #0 -stretch
    # установка заголовков
    foreach j $column_list {
        .frm_work.tree_work heading $j -text $j
    }
    incr work_list_row_count
}

ที่นี่ แทนที่จะใช้คำสั่งง่ายๆ [split $str ///] ซึ่งแยกสตริงออกเป็นองค์ประกอบโดยคั่นด้วย /// และส่งคืนรายการ จะใช้นิพจน์ทั่วไป เนื่องจากองค์ประกอบบางอย่างมีโคลอนด้วย

ขั้นตอน InsertClusterItems (หนึ่งในหลาย ๆ รายการที่คล้ายกัน) เพียงเพิ่มรายการองค์ประกอบลูกพร้อมตัวระบุที่เกี่ยวข้องลงในแผนผังขององค์ประกอบคลัสเตอร์ที่ต้องการ
แทรกClusterItems

proc InsertClusterItems {tree id} {
    set parent "cluster::$id"
    $tree insert $parent end -id "infobases::$id" -text "Информационные базы" -values "$id"
    $tree insert $parent end -id "servers::$id" -text "Рабочие серверы" -values "$id"
    $tree insert $parent end -id "admins::$id" -text "Администраторы" -values "$id"
    $tree insert $parent end -id "managers::$id" -text "Менеджеры кластера" -values $id
    $tree insert $parent end -id "processes::$id" -text "Рабочие процессы" -values "workprocess-all"
    $tree insert $parent end -id "sessions::$id" -text "Сеансы" -values "sessions-all"
    $tree insert $parent end -id "locks::$id" -text "Блокировки" -values "blocks-all"
    $tree insert $parent end -id "connections::$id" -text "Соединения" -values "connections-all"
    $tree insert $parent end -id "profiles::$id" -text "Профили безопасности" -values $id
}

คุณสามารถพิจารณาอีกสองตัวเลือกสำหรับการดำเนินการตามขั้นตอนที่คล้ายกันซึ่งจะเห็นได้ชัดเจนว่าคุณสามารถเพิ่มประสิทธิภาพและกำจัดคำสั่งซ้ำ ๆ ได้อย่างไร:

ในขั้นตอนนี้ การเพิ่มและการตรวจสอบจะได้รับการแก้ไขโดยตรง:

แทรก BaseItems

proc InsertBaseItems {tree id} {
    set parent "infobase::$id"
    if { [$tree exists "sessions::$id"] == 0 } {
        $tree insert $parent end -id "sessions::$id" -text "Сеансы" -values "$id"
    }
    if { [$tree exists "locks::$id"] == 0 } {
        $tree insert $parent end -id "locks::$id" -text "Блокировки" -values "$id"
    }
    if { [$tree exists "connections::$id"] == 0 } {
        $tree insert $parent end -id "connections::$id" -text "Соединения" -values "$id"
    }
}

นี่เป็นแนวทางที่ถูกต้องมากขึ้น:

แทรกรายการโปรไฟล์

proc InsertProfileItems {tree id} {
    set parent "profile::$id"
    set lst {
        {dir "Виртуальные каталоги"}
        {com "Разрешённые COM-классы"}
        {addin "Внешние компоненты"}
        {module "Внешние отчёты и обработки"}
        {app "Разрешённые приложения"}
        {inet "Ресурсы интернет"}
    }
    foreach i $lst {
        append item [lindex $i 0] "::$id"
        if { [$tree exists $item] == 0 } {
            $tree insert $parent end -id $item -text [lindex $i 1] -values "$id"
        }
        unset item 
    }
}

ความแตกต่างระหว่างพวกเขาคือการใช้การวนซ้ำซึ่งคำสั่งซ้ำจะถูกดำเนินการ วิธีการใช้จะขึ้นอยู่กับดุลยพินิจของผู้พัฒนา

เราได้กล่าวถึงการเพิ่มองค์ประกอบและการเรียกข้อมูลแล้ว ตอนนี้ก็ถึงเวลามุ่งเน้นไปที่การแก้ไข เนื่องจากโดยพื้นฐานแล้ว พารามิเตอร์เดียวกันนี้ใช้สำหรับการแก้ไขและเพิ่ม (ยกเว้นฐานข้อมูล) จึงใช้แบบฟอร์มกล่องโต้ตอบเดียวกัน อัลกอริทึมสำหรับการเรียกขั้นตอนการเพิ่มมีลักษณะดังนี้:

เพิ่ม::$key->AddToplevel

และสำหรับการแก้ไขเช่นนี้:

แก้ไข::$key->เพิ่ม::$key->AddTopLevel

ตัวอย่างเช่น เรามาแก้ไขคลัสเตอร์กัน เช่น เมื่อคลิกที่ชื่อของคลัสเตอร์ในแผนผังแล้ว ให้กดปุ่มแก้ไขในแถบเครื่องมือ (ดินสอ) และแบบฟอร์มที่เกี่ยวข้องจะปรากฏบนหน้าจอ:

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk
แก้ไข::คลัสเตอร์

proc Edit::cluster {tree host values} {
    global default lifetime_limit expiration_timeout session_fault_tolerance_level
    global max_memory_size max_memory_time_limit errors_count_threshold security_level
    global load_balancing_mode kill_problem_processes active_cluster 
    agent_user agent_pwd cluster_user cluster_pwd auth
    if {$cluster_user ne "" && $cluster_pwd ne ""} {
        set auth "--cluster-user=$cluster_user --cluster-pwd=$cluster_pwd"
    } else {
        set auth ""
    }
    # рисуем форму для кластера
    set frm [Add::cluster $tree $host $values]
    # меняем текст на метке
    $frm configure -text "Редактирование кластера"
    
    set active_cluster $values
    # получаем данные по выделенному кластеру
    set lst [RunCommand cluster::$values "cluster info --cluster=$active_cluster $host"]
    # заполняем поля
    FormFieldsDataInsert $frm $lst
    # выключаем поля, редактирование которых запрещено
    $frm.ent_host configure -state disable
    $frm.ent_port configure -state disable
    # переназначаем обработчик
    .add.frm_btn.btn_ok configure -command {
        RunCommand "" "cluster update 
        --cluster=$active_cluster $auth 
        --name=[.add.frm.ent_name get] 
        --expiration-timeout=$expiration_timeout 
        --lifetime-limit=$lifetime_limit 
        --max-memory-size=$max_memory_size 
        --max-memory-time-limit=$max_memory_time_limit 
        --security-level=$security_level 
        --session-fault-tolerance-level=$session_fault_tolerance_level 
        --load-balancing-mode=$load_balancing_mode 
        --errors-count-threshold=$errors_count_threshold 
        --kill-problem-processes=$kill_problem_processes 
        $auth $host"
        $tree delete "cluster::$active_cluster"
        Run::server $tree $host ""
        destroy .add
    }
}

ตามความคิดเห็นในโค้ด โดยหลักการแล้ว ทุกอย่างชัดเจน ยกเว้นว่าโค้ดตัวจัดการปุ่มถูกแทนที่และมีขั้นตอน FormFieldsDataInsert ที่กรอกข้อมูลลงในฟิลด์และเตรียมใช้งานตัวแปร:

FormFieldsDataInsert

proc FormFieldsDataInsert {frm lst} {
    foreach i [lindex $lst 0] {
        # получаем список параметров и значений
        if [regexp -nocase -all -- {(D+)(s*?|)(:)(s*?|)(.*)} $i match param v2 v3 v4 value] {
            # меняем символы
            regsub -all -- "-" [string trim $param] "_" entry_name
            # заполняем данными
            if [winfo exists $frm.ent_$entry_name] {
                $frm.ent_$entry_name delete 0 end
                $frm.ent_$entry_name insert end [string trim $value """]
            }
            if [winfo exists $frm.cb_$entry_name] {
                global $entry_name
                set $entry_name [string trim $value """]
            }
            # для чекбоксов меняем значения
            if [winfo exists $frm.check_$entry_name] {
                global $entry_name
                if {$value eq "0"} {
                    set $entry_name no
                } elseif {$value eq "1"} {
                    set $entry_name yes
                } else {
                    set $entry_name $value
                }
            }
        }
    }
}

ในขั้นตอนนี้ข้อดีอีกประการหนึ่งของ tcl ปรากฏขึ้น - ค่าของตัวแปรอื่น ๆ จะถูกแทนที่ด้วยชื่อตัวแปร เหล่านั้น. เพื่อให้การกรอกแบบฟอร์มและการเริ่มต้นตัวแปรเป็นแบบอัตโนมัติ ชื่อของฟิลด์และตัวแปรจะสอดคล้องกับสวิตช์บรรทัดคำสั่งของยูทิลิตี้ rac และชื่อของพารามิเตอร์เอาต์พุตคำสั่งโดยมีข้อยกเว้นบางประการ - เส้นประจะถูกแทนที่ด้วยขีดล่าง เช่น กำหนดเวลางานปฏิเสธ ตรงกับสนาม ent_scheduled_jobs_deny และตัวแปร กำหนดเวลา_งาน_ปฏิเสธ.

แบบฟอร์มสำหรับการเพิ่มและแก้ไขอาจแตกต่างกันไปตามองค์ประกอบของฟิลด์ เช่น การทำงานกับฐานข้อมูล:

การเพิ่มความปลอดภัยของข้อมูล

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

การแก้ไขความปลอดภัยของข้อมูล

กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

ในขั้นตอนการแก้ไข Edit::infobase ช่องที่ต้องกรอกจะถูกเพิ่มลงในแบบฟอร์ม รหัสมีขนาดใหญ่ ดังนั้นฉันจึงไม่นำเสนอที่นี่

โดยการเปรียบเทียบ ขั้นตอนการเพิ่ม แก้ไข ลบจะถูกนำไปใช้กับองค์ประกอบอื่นๆ

เนื่องจากการทำงานของยูทิลิตี้หมายถึงเซิร์ฟเวอร์ คลัสเตอร์ ฐานข้อมูล ฯลฯ จำนวนไม่จำกัด เพื่อกำหนดว่าคลัสเตอร์ใดเป็นของเซิร์ฟเวอร์หรือระบบความปลอดภัยของข้อมูล จึงได้มีการแนะนำตัวแปรส่วนกลางหลายตัว โดยค่าต่างๆ จะถูกตั้งค่าไว้แต่ละตัว เวลาที่คุณคลิกที่องค์ประกอบของต้นไม้ เหล่านั้น. ขั้นตอนจะวนซ้ำผ่านองค์ประกอบหลักทั้งหมดและตั้งค่าตัวแปร:

ตั้งค่า GlobalVarFromTreeItems

proc SetGlobalVarFromTreeItems {tree id} {
    global host server active_cluster infobase
    set parent [$tree parent $id]
    set values [$tree item $id -values]
    set key [lindex [split $id "::"] 0]
    switch -- $key {
        server {set host $values}
        work_server {set server $values}
        cluster {set active_cluster $values}
        infobase {set infobase $values}
    }
    if {$parent eq ""} {
        return
    } else {
        SetGlobalVarFromTreeItems $tree $parent
    }
}

คลัสเตอร์ 1C ช่วยให้คุณสามารถทำงานโดยมีหรือไม่มีการอนุญาต ผู้ดูแลระบบมีสองประเภท ได้แก่ ผู้ดูแลระบบเอเจนต์คลัสเตอร์และผู้ดูแลระบบคลัสเตอร์ ดังนั้น เพื่อการดำเนินการที่ถูกต้อง จึงได้มีการแนะนำตัวแปรส่วนกลางอีก 4 ตัวซึ่งประกอบด้วยข้อมูลเข้าสู่ระบบและรหัสผ่านของผู้ดูแลระบบ เหล่านั้น. หากมีบัญชีผู้ดูแลระบบในคลัสเตอร์ กล่องโต้ตอบจะปรากฏขึ้นเพื่อป้อนข้อมูลเข้าสู่ระบบและรหัสผ่านของคุณ ข้อมูลจะถูกบันทึกไว้ในหน่วยความจำและแทรกลงในแต่ละคำสั่งสำหรับคลัสเตอร์ที่เกี่ยวข้อง

นี่เป็นความรับผิดชอบของขั้นตอนการจัดการข้อผิดพลาด

เกิดข้อผิดพลาดในการแยกวิเคราะห์

proc ErrorParcing {err opt} {
    global cluster_user cluster_pwd agent_user agent_pwd
        switch -regexp -- $err {
        "Cluster administrator is not authenticated" {
            AuthorisationDialog "Администратор кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set cluster_user [.auth_win.frm.ent_name get]
                set cluster_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
            #RunCommand $root $par
        }
        "Central server administrator is not authenticated" {
            AuthorisationDialog "Администратор агента кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set agent_user [.auth_win.frm.ent_name get]
                set agent_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
        }
        "Администратор кластера не аутентифицирован" {
            AuthorisationDialog "Администратор кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set cluster_user [.auth_win.frm.ent_name get]
                set cluster_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
            #RunCommand $root $par
        }
        "Администратор центрального сервера не аутентифицирован" {
            AuthorisationDialog "Администратор агента кластера"
            .auth_win.frm_btn.btn_ok configure -command {
                set agent_user [.auth_win.frm.ent_name get]
                set agent_pwd [.auth_win.frm.ent_pwd get]
                destroy .auth_win
            }
        }
        (.+) {
            tk_messageBox -type ok -icon error -message "$err"
        }
    }
}

เหล่านั้น. ขึ้นอยู่กับสิ่งที่คำสั่งส่งกลับ ปฏิกิริยาจะเป็นไปตามนั้น

ในขณะนี้มีการใช้งานฟังก์ชันการทำงานประมาณ 95 เปอร์เซ็นต์ สิ่งที่เหลืออยู่คือใช้งานกับโปรไฟล์ความปลอดภัยและทดสอบ =) นั่นคือทั้งหมดที่ ฉันขอโทษสำหรับเรื่องราวยู่ยี่

รหัสนี้มีให้ใช้งานตามปกติ ที่นี่.

อัปเดต: ฉันทำงานกับโปรไฟล์ความปลอดภัยเสร็จแล้ว ขณะนี้ฟังก์ชันการทำงานได้รับการติดตั้งแล้ว 100%

อัปเดต 2: เพิ่มการแปลเป็นภาษาอังกฤษและรัสเซียแล้ว ทดสอบการทำงานใน win7 แล้ว
กำลังเขียน GUI สำหรับ 1C RAC หรืออีกครั้งเกี่ยวกับ Tcl/Tk

ที่มา: will.com

เพิ่มความคิดเห็น