Два варианта использования графического пакета IUP (Lua 5.4) в QUIKе

Страницы: 1
RSS
Два варианта использования графического пакета IUP (Lua 5.4) в QUIKе
 
Инструкция в коде примера:
Код

--- Использование графического пакета IUP (Lua 5.4...) в QUIKе (! ограничение IUP: 
-- только в одном из запущенном скрипте экземпляра QUIK).
-- Доступны все возможности пакета IUP (подключаемой версии).
-- Вариант 1: запуск IUP в потоке main (надо подключать только IUP, но цикл обработки скрипта программировать 
-- в таймере IUP: помечено #### ).
-- Вариант 2: запуск IUP в отдельном потоке отличном от main(кроме IUP, обязательно подключать пакет QluaUser).
--    Вариант 1 более безопасный, чем 2. Так как однопоточный, и не требующий синхронизации диалога с 
-- основной работой скрипта.
-- Ссылка для скачивания пакетов iup.dll, iuplua54.dll, QluaUser.dll (мой):
--    https://cloud.mail.ru/public/7xAm/jaCgULqGo    Перед использованием пакетов, надо их разблокировать.
-- Пакеты переслать в папку экземпляра QUIK, хранящую файл info.exe.
_RUN_ = true

function main()
   -- Пример формы IUP со двумя вкладками ---
   local Pause = 1000
   local thread = false  -- false - выполнение диалога в потоке main; true - запуск IUP в отдельном потоке --
   local cpath = (getWorkingFolder() .. '\\?54.dll;' .. getWorkingFolder() .. '\\?_5_4.dll;'.. getWorkingFolder() .. '\\?.dll;' .. package.cpath)
    ---
   package.cpath = cpath
   require("iuplua")
   local QluaUser = require('QluaUser')  -- Это для запуска диалога IUP в отдельном потоке ---
   if thread then
      QluaUser = require('QluaUser')
   end
   local function iup_form()
      local timer = iup.timer { time = Pause }   ---- Вместо sleep таймер формы  ( млсек.)  ---
      timer.run = "NO"
      function timer:action_cb()
         if not _RUN_ then
            timer.run = "NO"
            iup:ExitLoop()   --  Завершает текущий диалог      
         end
         if not thread then -- Основной цикл обработки скрипта в таймере IUP ---
            -- Тело основного цикла скрипта --  ####
            message('iup выполняется в потоке main. Тело основного цикла скрипта должно выполняеться здесь (в таймере iup)')
            -------------------
         end
      end
      -- Creates boxes
      local vboxA = iup.vbox{iup.fill{}, iup.label{title="TABS AAA", expand="HORIZONTAL"}, iup.button{title="AAA"}}
      local vboxB = iup.vbox{iup.label{title="TABS BBB"}, iup.button{title="BBB"}}
      -- Sets titles of the vboxes
      vboxA.tabtitle = "AAAAAA"
      vboxB.tabtitle = "BBBBBB"
      -- Creates tabs 
      local tabs = iup.tabs{vboxA, vboxB}
      -- Creates dialog
      local dlg = iup.dialog{iup.vbox{tabs; margin="10x10"}; title="Test IupTabs", size="150x80"}
      -- Shows dialog in the center of the screen
      dlg:showxy(iup.CENTER, iup.CENTER)
      timer.run = "YES"
      if (iup.MainLoopLevel()==0) then
        iup.MainLoop()
      end
      timer.run = "NO"
   end
   if thread then
      QluaUser.ThreadNew(iup_form)
   else
      iup_form()
      iup.Close()
      iup = nil
   end
   
   while _RUN_ do    -- Основной цикл обработки скрипта в main ---
      -----------------------
      if thread then
         message('iup был запущен в отдельном потоке. Тело основного цикла скрипта выполняется в потоке main.')
      else
         message('После завершения потока iup, продолжается выполняется поток main.')
      end
      sleep(Pause)
      -----------------------
   end
end 
---
function OnStop(flag)
   _RUN_ = false
   sleep(2000)
   if iup then iup.Close() end
   return 3000 
end
 
Просто к слову, т.к. используется неизвестная библиотека QluaUser.dll то запускать это не видя исходники будут сложно. По крайней мере мне, не знаю как другим.
 
Цитата
Nikolay написал:
Просто к слову, т.к. используется неизвестная библиотека QluaUser.dll то запускать это не видя исходники будут сложно. По крайней мере мне, не знаю как другим.

Цитата
TGB написал:
-- Вариант 1: запуск IUP в потоке main (надо подключать только IUP, но цикл обработки скрипта программировать
-- в таймере IUP: помечено #### ).
-- Вариант 2: запуск IUP в отдельном потоке отличном от main(кроме IUP, обязательно подключать пакет QluaUser).
--    Вариант 1 более безопасный, чем 2. Так как однопоточный, и не требующий синхронизации диалога с
-- основной работой скрипта.
 
Цитата
TGB написал:
Цитата
Nikolay написал:
Просто к слову, т.к. используется неизвестная библиотека QluaUser.dll то запускать это не видя исходники будут сложно. По крайней мере мне, не знаю как другим.

Цитата
TGB написал:
-- Вариант 1: запуск IUP в потоке main (надо подключать только IUP, но цикл обработки скрипта программировать
-- в таймере IUP: помечено #### ).
-- Вариант 2: запуск IUP в отдельном потоке отличном от main(кроме IUP, обязательно подключать пакет QluaUser).
--    Вариант 1 более безопасный, чем 2. Так как однопоточный, и не требующий синхронизации диалога с
-- основной работой скрипта.
Я понял про что это библиотека. Я говорил, что нет исходников.
 
Цитата
Nikolay написал:
Я понял про что это библиотека. Я говорил, что нет исходников.

Мною рекомендуется использовать первый вариант и для него пакет QluaUser.dll не требуется.
 
Нет, мне это не надо. Просто замечание, что если предлагаете какое-то открытое решение, то зачем скрывать код, его подключающее.
 
Цитата
Nikolay написал:
Просто замечание, что если предлагаете какое-то открытое решение, то зачем скрывать код, его подключающее.
 С этим можно бы согласиться, но есть вариант 1, в котором нет скрытых кодов.
 
Цитата
Nikolay написал:
Нет, мне это не надо.
   Согласен, в работающем роботе достаточно возможностей пользовательских таблиц QUIK. Идеальный, зарабатывающий робот должен это делать без вводных и без лишнего вывода, только отчеты о прибыли в журнале с аналитикой, полезной пользователю. Реально, желателен вывод роботом сообщений о наступлении событий, требующих вмешательство пользователя. Все это без проблем реализуемо на таблицах QUIK.
  Графический пакет я использовал для реализации средств создания скриптов, что сделать на таблицах QUIK сложновато.
 
Статья не разъясняет, "для чего и как". Поэтому нет причины углубляться в скрипт.
 
Вообще-то есть и третий способ использования IUP в Quik'е ttps://forum.quik.ru/forum10/topic9267/Со вполне чётко разъяснённой мотивацией и  механикой - параллельное исполнение скрипта main() и ручных манипуляций в IUP с непрерывной связью между этими нитями (threads) в обоих направлениях. Переключение сопрограмм-корутин (coroutines) по таймеру между потоками управления в main() и IUP должно быть эффективнее любого другого способа.

А вот ещё четвёртый способ - запустить IUP из отдельного скрипта со своим main(), а ещё лучше - отдельным от Quik'а приложением; тогда не будет лишней нагрузки на Quik. И держать двустороннюю связь между торговым скриптом и IUP через DDE. Это создаст ещё меньшие накладные расходы (в основе DDE - оконные процедуры Windows), и отпадает необходимость переключения по таймеру - связь только когда возникает реальная потребность обмена.
 
1.
Цитата
Ростислав Дм. Кудряшов написал:
Статья не разъясняет, "для чего и как". Поэтому нет причины углубляться в скрипт.
  Где вы в этой ветке прочитали мою статью?
2.
Цитата
Ростислав Дм. Кудряшов написал:
есть и третий способ использования IUP
  Я это читал.
3.
Цитата
Ростислав Дм. Кудряшов написал:
параллельное исполнение скрипта main() и ручных манипуляций в IUP с непрерывной связью между этими нитями (threads) в обоих направлениях.
  Где вы прочитали, что корутины в Lua выполняются параллельно? Интересно, как в одном потоке можно с помощью корутин реализовать параллелизм (одновременность) их выполнения? Но на всякий случай цитата Р. Иерузалимски: "в любой момент времени программа с сопрограммами выполняет только одну из своих сопрограмм".
 
Цитата
TGB написал:
1.  
Цитата
Ростислав Дм. Кудряшов написал:
Статья не разъясняет, "для чего и как". Поэтому нет причины углубляться в скрипт.
   Где вы в этой ветке прочитали мою статью?
2.  
Цитата
Ростислав Дм. Кудряшов написал:
есть и третий способ использования IUP
   Я это читал.
3.  
Цитата
Ростислав Дм. Кудряшов написал:
параллельное исполнение скрипта main() и ручных манипуляций в IUP с непрерывной связью между этими нитями (threads) в обоих направлениях.
   Где вы прочитали, что корутины в Lua выполняются параллельно? Интересно, как в одном потоке можно с помощью корутин реализовать параллелизм (одновременность) их выполнения? Но на всякий случай цитата Р. Иерузалимски: "в любой момент времени программа с сопрограммами выполняет только одну из своих сопрограмм".
 
Цитата
Ростислав Дм. Кудряшов написал:
Цитата
TGB написал:
1.  
Цитата
Ростислав Дм. Кудряшов  написал:
Статья не разъясняет, "для чего и как". Поэтому нет причины углубляться в скрипт.
    Где вы в этой ветке прочитали мою статью?
2.  
Цитата
Ростислав Дм. Кудряшов  написал:
есть и третий способ использования IUP
    Я это читал.
3.  
Цитата
Ростислав Дм. Кудряшов  написал:
параллельное исполнение скрипта main() и ручных манипуляций в IUP с непрерывной связью между этими нитями (threads) в обоих направлениях.
    Где вы прочитали, что корутины в Lua выполняются параллельно? Интересно, как в одном потоке можно с помощью корутин реализовать параллелизм (одновременность) их выполнения? Но на всякий случай цитата Р. Иерузалимски: "в любой момент времени программа с сопрограммами выполняет только одну из своих сопрограмм".
 
Не поленись, а запусти на своём ПК пример переключения спорограмм-корутин между IUP и main(). И убедишься, что невытесняющая (кооперативная) многозадачность порой лучше вытесняющей.
 
Цитата
Ростислав Дм. Кудряшов написал:
Не поленись, а запусти на своём ПК пример переключения спорограмм-корутин между IUP и main(). И убедишься, что невытесняющая (кооперативная) многозадачность порой лучше вытесняющей.
   С этого места, пожалуйста, поподробнее. Как вам удалось сумев запустить единственный готовый пример, определить что он порой лучше вытесняющей многозадачности?  С чем вы его сравнивали? Или вы можете определять лучшее без сравнения? Откуда к вам приходят такие озарения :smile: ? Поделитесь.
 
Опять эти разговоры про параллельное исполнение. Lua - синхронное, однопоточное изделие. Корутины - это просто созданный стек и выполнение через переключение. Работать может либо тело скрипта, либо корутина, но не параллельное исполнение. Как только вы начинаете с помощью внешних средств пробовать сделать параллельное исполнение, то сразу же возникает вопрос о защите стека, т.к. всегда возникнет ситуация когда кто-то пишет в стек, а другой, параллельный, читает и очищает стек. Квик в этом плане падает сразу, если небрежно уронить стек скрипта. Что отдельная тема, прочему скрипт убивает весь терминал.

Так что реализация работы с одним стеком через потоки - это не такая простая задача, и точно не про корутины, которые не стоит путать с таковыми в других языках, приписывая им те же свойства. Lua - это продукт из 90-х.
 
От теории к смыслам! :smile:
Вся прелесть и заключается использования сопрограмм в превращении выполнения одной последовательной задачи луа в ряд независимых задач! Как добиваемся независимости, опросом сопрограмм, там где нужно и тогда когда нужно!

Вот мой рабочий пример (как есть прямо в таком виде сейчас крутится, это не образец для подражания,  :what: но), что я только с ним не делал? И сейчас на вечерней падает сопрограмма - обработка таблицы всех сделок, ну и ладно скрипт работает, а с вечеркой когда руки дойдут разберусь.
Код
function OnMain()

    Log:trace("OnMain started")
    
    -- Создаем корутины Прверка пароля
    local coPassword   = coroutine.create(checkPassword)
    
    -- Прверка пароля при первом входе!(корутина с безопасным вызовом)
    safe_resume(coPassword, "coPassword")

    --[[GUI.initialize()

    -- Создание таблицы ордеров
    GUI.createTable({
        name = "orders",
        title = "Active Orders",
        width = 1000,
        height = 400,
        x = 10,
        y = 50,
        columns = {
            {
                iCode = 1,
                title = "Time",
                par_type = QTABLE_TIME_TYPE,
                width = 120
            },
            {
                iCode = 2,
                title = "Price",
                par_type = QTABLE_DOUBLE_TYPE,
                width = 100
            },
            {
                iCode = 3,
                title = "Volume",
                par_type = QTABLE_INT64_TYPE,
                width = 100
            }
        }
    })
    
    -- Создание информационной метки
    GUI.createLabel({
        id = "statusLabel",
        text = "System Status: OK",
        x = 10,
        y = 10,
        width = 300,
        color = "#00FF00",
        bgColor = "#333333"
    })
    --]]

    GUI.initialize()
    -- Создание таблицы ордеров
    GUI.createTable({
        name = "orders",
        title = "Активные ордера",
        columns = {
            {
                iCode = 1,
                title = "Время",
                type = QTABLE_TIME_TYPE,
                width = 120
            },
            {
                iCode = 2,
                title = "Операция",
                type = QTABLE_STRING_TYPE,
                width = 100
            },
            {
                iCode = 3,
                title = "Объем",
                type = QTABLE_INT_TYPE, -- Теперь правильно QTABLE_INT_TYPE = 1
                width = 80
            }
        }
    })

    -- Пример использования в главном цикле
    local hook = PerformanceHook()
    --local performanceHook = PerformanceHook()

    local fatal = nil

    AutoUpdateVersion()
    CreateWindowRobot()

    if Start ~= nil then
        Start()
    end

    AtExit(function()
        for _, so in pairs(SmartOrder.pool) do
            so:enough()
            so:process()
        end
    end)

    if Robot ~= nil then -- Провека на наличие основтой функции

        ---- функция для получения информации о классе 
        local ClassInfo = getClassInfo(class_names[1]);
        firmid = ClassInfo and ClassInfo.firmid or ''; Log:trace('firmid = ' .. tostring(firmid ) );
        
        -- Создаем корутину для функции Robot
        local routine = coroutine.create(Robot) 
        Log:trace("Robot started")
        
        -- Создаем корутины
        --local coPassword   = coroutine.create(checkPassword)
        local coConnection = coroutine.create(checkConnection)
        local coTradeDate  = coroutine.create(checkTradeDate)
        local coServerTime = coroutine.create(checkServerTime)
        local coWorkTime   = coroutine.create(checkWorkTime) -- Сопрограмма для отслеживания рабочего времени 
        --local coStopTimeTrading = coroutine.create(checkStopTimeTrading)

        -- Cоздание корутин для стратегий реального времени
        --if not rejime.test and event_co.connected == 1 and event_co.servertime[2] and flagTimeTrading then
            -- Cоздание корутины function 
            local coCapitalManager = coroutine.create(CapitalManagerCo--function() CapitalManagerCo(CONFIG) end
            )
            local coMoneyManagement = coroutine.create(function()
              --for _, class in ipairs(class_names) do
                --for n, symbol in ipairs(symbol_names) do  
                    --if mm[class][symbol] ~= nil then
                        --MoneyManagementCo( mm[class][symbol] )
                        MoneyManagementCo( firmid, account, class_names, symbol_names )
                    --else
                        --Log:error("MoneyManagement instance 'mm[n]' is nil. Cannot start MoneyManagementCoroutine.")
                    --end
                --end
              --end
            end)

        --if not rejime.test and event_co.connected == 1 and event_co.servertime[2] and flagTimeTrading then
            local co_alltrade = coroutine.create(alltrade_processor)
            local co_order_book = coroutine.create(order_book_processor)
            --local co_tt_parametrs = coroutine.create(trade_data_processor)
            
            local futuresCoroutine = coroutine.create(futures_position_processor)
            local futuresLimitCoroutine = coroutine.create(futures_limit_processor)
        --end
        
           
        while WORKING_FLAG do

            --------------------------
            local sek = os.clock();
            --------------------------

            --[[-- Обновление данных
            --GUI.update_connection_status(isConnected())
            --GUI.update_last_activity(os.time())
            --GUI.update_uptime(getUptime())

            -- Добавление новых ордеров
            --while hasNewOrders() do
            --    GUI.add_order(getNextOrder())
            --end

            -- Обновление торговой панели
            GUI.updateComponent("trading_panel", {
                {"BUY", 100.50, 10},
                {"SELL", 101.00, 5}
            })

            -- Добавление метки на график
            GUI.Chart.addLabel("MAIN_CHART", "Support Level", {
            YVALUE = 98.50,
            COLOR = RGB(46, 204, 113)
            })--]]

            -- Обновление данных
            GUI.updateTable("orders", {
                {os.date("%H:%M:%S"), "BUY", 10},
                {os.date("%H:%M:%S"), "SELL", 5}
            })

            -- Запуск корутин с безопасным вызовом
            if not rejime.test then

                safe_resume(coConnection, "coConnection")
                SetCell(table_id, 1, 1, tostring(event_co.connected == 1 and 'Connect' or 'NoConnect'))

                if event_co.connected == 1 then

                    safe_resume(coTradeDate,  "coTradeDate")
                    safe_resume(coServerTime, "coServerTime")
                    safe_resume(coWorkTime,   "coWorkTime")
                    --safe_resume(coStopTimeTrading, "coStopTimeTrading")

                    --if event_co.connected == 1 then
                    -- Запуск корутины MoneyManagement
                    --safe_resume( MoneyManagementRoutine, "MoneyManagementRoutine")
                    safe_resume( coCapitalManager, "coCapitalManager")
                    --end

                    -- Запускаем корутину (История сделок и стакан)
                    --Log:info('tradedate[1] =' ..tostring(event_co.tradedate[1]) ..'; worktime =' ..tostring(event_co.worktime[2]) ..'; servertime[2] =' ..tostring(event_co.servertime[2]) --..'; flagTimeTrading =' ..tostring(flagTimeTrading) )
                    
                    if event_co.tradedate[1] 
                    and event_co.worktime[2]
                    and event_co.servertime[2] 
                    then
                        safe_resume(co_alltrade, 'co_alltrade')
                        safe_resume(co_order_book, 'co_order_book')
                    end

                    --coroutine.resume(co_tt_parametrs)
                    -- Функция для запуска корутины
                    --start_futures_position_processing(futuresCoroutine)
                    --start_futures_limit_processing(futuresLimitCoroutine)

                    safe_resume(futuresCoroutine, 'futuresCoroutine')
                    --safe_resume(futuresLimitCoroutine, 'futuresLimitCoroutine')
                end

                -- Формирем модуль СМ
                --safe_resume( coCapitalManager, "coCapitalManager")
                --safe_resume( coMoneyManagement, "coMoneyManagement")
            end

            -- co Robot Затем используйте его методы
            local res, errmsg = coroutine.resume(routine)
            
            if not res then
                fatal = "Broken coroutine (Robot): " .. errmsg
                -- Вариант 1: Использование глобального обработчика (рекомендуется)
                _G.global_error_handler:handle_error(fatal, "Robot")
    
                -- Вариант 2: Статический вызов
                -- ErrorHandler.handle_error_static(fatal, "Robot")
                break
            end

            if coroutine.status(routine) == "dead" then
                Log:trace("Robot routine finished")
                break
            end
           
            ProcessRegistered()

            --local elapsed_ms = performanceHook(sek) -- sleep(1000)
            hook(sek)  -- Обновляем метрики

            sleep(spinner.config.interval * 1000)  -- Контроль частоты

            --if rejime.test then 
            --    sleep(100)
            --else
            --    Log:trace(math.floor(1000 - tonumber(elapsed_ms)))
                --sleep(  )
            --    sleep(1000)
            --end
        end
    end

    if Stop ~= nil then Stop() end
    ProcessAtExit()  -- Вызов ProcessAtExit
    ErrorCollector.log_all_errors()  -- Логирование всех ошибок при завершении
    Log:trace("Robot stopped")
    
    if fatal ~= nil then
        error(fatal)
    end
end
 
Конкретные смыслы и практические преимущества, или как сопрограммы меняют парадигму!

Смысл 1: Простота - читаемость и ясность кода.
--------------------------------------------------------

* Теория: Мы пишем последовательный код.
* Смысл: Мы можем описать сложную асинхронную логику так, как мы о ней думаем — шаг за шагом, без нагромождения хаоса колбэков.
"Ад колбэков" — код растет вглубь, асинхронная логика размазана по функциям. После (красота сопрограмм)!
Код
function сложнаяЗадача()
    local data = загрузитьДанные("url") -- Здесь может быть yield
    local result = обработатьДанные(data) -- И здесь
    local success = сохранитьВБД(result) -- И здесь
    если success then
        обновитьИнтерфейс()
    end
end
-- Создаем сопрограмму и "опросом" управляем ее выполнением
local task = coroutine.create(сложная Задача)
Смысл: Код выглядит как обычный, линейный и "блокирующий", но на деле он асинхронный. Мозг читает его без напряжения.


Смысл 2: Архитектор - "Кооперативная многозадачность" и контроль.
------------------------------------------------------------
* Теория: Мы сами решаем, когда отдать управление.
* Смысл: Мы становимся архитекторами своего планировщика. Мы не зависим от прерываний по таймеру, а сами говорим: "Стоп, я сейчас подожду, а ты пока выполни что-то еще".
Код
-- Пул из нескольких "независимых задач", как вы и сказали
local tasks = {
    coroutine.create(function() ... end), -- Задача 1: загрузка контента
    coroutine.create(function() ... end), -- Задача 2: анимация UI
    coroutine.create(function() ... end), -- Задача 3: обработка ввода
}

-- Главный цикл "опросом" управляет всеми задачами
function главныйЦикл()
    while true do
        local всеЗавершены = true
        for i, co in ipairs(tasks) do
            if coroutine.status(co) ~= "dead" then
                всеЗавершены = false
                coroutine.resume(co) -- "Опрос" конкретной сопрограммы!
            end
        end
        если все Завершены then break end
    end
end
* Смысл: Мы точно знаем, в какой момент какая задача будет выполняться. Нет гонки данных, нет сложных примитивов синхронизации, как в потоках. Мы создаем детерминированную систему.


Смысл 3: Эффективность - эффективное ожидание и реактивность.
------------------------------------------------------------------------------------------------------
* Теория: Сопрограмма может быть приостановлена в любой точке.
* Смысл: Мы можем элегантно реализовать ожидание без блокировки всего приложения.
Код
-- В движке: ждем 2 секунды перед загрузкой данных.
function ждать(секунды)
    local времяСтарта = os.clock()
    while os.clock() - времяСтарта < секунды do
        coroutine.yield() -- "Спим", не блокируя основной поток, отдавая управление другим задачам
    end
end
* Смысл:** Мы пишем логику, которая "живет" во времени, но не замораживает весь мир вокруг себя. Это основа для AI, анимаций, диалогов в играх.


Смысл 4: Эффективность - генераторы и бесконечные последовательности.
------------------------------------------------------------------------------------------------------------------
* Теория: Сопрограмма может yield-ить промежуточные результаты.
* Смысл: Мы создаем "ленивые" вычисления и потенциально бесконечные потоки данных.
Код
function бесконечнаяПоследовательность(старт, шаг)
    local текущее = старт
    while true do
        coroutine.yield(текущее)
        текущее = текущее + шаг
    end
end

-- Создаем генератор чисел, начиная с 10, с шагом 5
local генератор = coroutine.wrap(бесконечнаяПоследовательность)
local gen = генератор(10, 5)

print(gen()) --> 10
print(gen()) --> 15
print(gen()) --> 20
-- Можно получать значения по требованию, не вычисляя все сразу.
* Смысл: Экономия памяти и вычислений. Мы рассчитываем следующее значение только тогда, когда оно реально нужно.

Итого. Вся прелесть — в этом "превращении". :smile:  Сопрограммы в Lua — это мост между двумя мирами:
1. Мир для программиста: Удобный, последовательный, понятный код.
2. Мир для машины: Гибкое, асинхронное, неблокирующее выполнение.

И ключ к управлению этим миром — наш "опрос" с помощью `coroutine.resume`. Это диалог: "Ты готова поработать?", "Сделай шаг", "Отдохни, я спрошу тебя позже".
Это и есть тот самый глубокий смысл, который делает сопрограммы в Lua таким элегантным и мощным инструментом.  
Страницы: 1
Читают тему
Наверх