In this post I’ll describe how to install Ruby and Tk. Tk is a GUI for Tcl (another script language) but it’s also used for Ruby. I recommend to read this tutorial first if this is the first time you are dealing with Tk. You’ll need some basic understanding of geometry (layout) managers, such as pack, grid and place.
Table of Contents
Windows 7 and Windows 10
The installation for Windows is the same for Windows 7 and Windows 10. It’s very straightforward. Go to http://rubyinstaller.org/downloads/ and download Ruby 2.1.7 (32-bit or 64-bit). When you start the install, make sure you select the Tcl/Tk support and add Ruby to your PATH.
Once installed, open a command prompt and type irb. When you see the interactive Ruby prompt, type require ‘tk’ and hit Enter. You should see True as result.
FreeBSD 10
Tcl, Tk and Ruby are standard in the ports tree, but the Ruby install doesn’t allow the configuration for Tk. Earlier, there was a port called ruby-tk, but it’s depreciated since Ruby 1.9. First, install Tcl and Tk.
pkg install tcl85 tk85
Download the tarball from Ruby’s website, unpack it and install it.
pkg install tcl85 tk85 wget https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz tar xvf ruby-2.1.7.tar.gz rm ruby-2.1.7.tar.gz cd ruby-2.1.7.tar.gz ./configure --without-ActiveTcl --with-tclConfig-dir=/usr/local/lib/tcl8.5 --with-tkConfig-dir=/usr/local/lib/tk8.5 --with-tcl-include=/usr/local/include/tcl8.5 --with-tk-include=/usr/local/include/tk8.5 --with-tcl-lib=/usr/local/lib --with-tk-lib=/usr/local/lib make make install
Do the same test, type irb and then type require ‘tk’. If you are running the test from a ssh session, you’ll receive an error that there is no display. This is fine, Ruby/Tk can’t initialize the graphic environment. You should test it from the GUI of your choice (Gnome, KDE, mate, xfce…)
CentOS 7
CentOS 7 install is also very simple. Install Ruby/Tk with:
yum install ruby ruby-tcltk
Same as the previous installs, type irb and then type require ‘tk’.
Example
I was playing a couple of days with Ruby/Tk and made a simple quiz program. You can get it from https://www.github.com/klimenta/GemQuiz or:
git clone https://www.github.com/klimenta/GemQuiz
The source code is also below and the instructions on how to make your own quiz. I am using the grid geometry manager. The source is not well documented and there are too many global variables, but it’s more an example of Tk rather than proper Ruby programming.
####################################################################################################################### #### The quiz has to be in the following format, no blank lines are allowed #### #### #### #### Question number #### #### Question text (one continuous line, the program takes care of the wrapping) #### #### Four possible answers, each line starts with A., B., C. and D. #### #### Answer(s) line that starts with the words "Answer: " (no quotes) and the correct answer #### #### The program takes care of single/multiple choice answer widgets #### ####################################################################################################################### #### e.g. #### ####################################################################################################################### #### Q1. #### #### Leveraged loans are loans provided to companies that already have a significant amount of outstanding debt. #### #### As a banker, how might you compare a leveraged loan to other loans in your portfolio? #### #### A. Higher risk to the lender but less costly to the borrower. #### #### B. Lower risk to the lender and less costly to the borrower. #### #### C. Lower risk to the lender but more costly to the borrower. #### #### D. Higher risk to the lender and more costly to the borrower. #### #### Answer: D #### ####################################################################################################################### #### This example will create a quiz with 4 radio buttons. If the answer line was Answer: B,C #### #### then the program will show four check buttons instead. #### #### NOTE: The question in this example is two lines. In the file, it should be one continuous line #### #### ####################################################################################################################### require 'tk' require 'tkextlib/tile' # Creates the root (parent) windows, 800 x 600, not resizable $root = TkRoot.new $root.minsize(width = 800, height = 600) $root.maxsize(width = 800, height = 600) $root.resizable(false, false) TkOption.add '*tearOff', 0 # Creates the menu with two options (open and exit) menuBar = TkMenu.new($root) $root['menu'] = menuBar menuFile = TkMenu.new(menuBar) menuBar.add :cascade, :menu => menuFile, :label => 'File' menuFile.add :command, :label => 'Open...', :command => proc{ openFile } menuFile.add :command, :label => 'Exit', :command => proc{ exitProgram } # Templates for the label's size and color Tk::Tile::Style.configure('Question.TLabel', {"font" => "helvetica 14", "foreground" => "black"}) Tk::Tile::Style.configure('QuestionNumber.TLabel', {"font" => "helvetica 20", "foreground" => "red"}) Tk::Tile::Style.configure('Correct.TLabel', {"font" => "helvetica 14", "foreground" => "darkgreen"}) Tk::Tile::Style.configure('Answer.TLabel', {"font" => "helvetica 14", "foreground" => "blue"}) Tk::Tile::Style.configure('Stats.TLabel', {"font" => "helvetica 10", "foreground" => "black"}) # Variables $arrQuestionLines = Array.new # All the question lines are stored here, e.g. Q1, Q2 $arrAnswerA = Array.new # All the possible answers that show up as answer A. $arrAnswerB = Array.new # All the possible answers that show up as answer B. $arrAnswerC = Array.new # All the possible answers that show up as answer C. $arrAnswerD = Array.new # All the possible answers that show up as answer D. $arrQuestions = Array.new # All the questions are stored in this array $arrCorrectAnswers = Array.new # These are the correct answers for the questions $arrUserAnswers = Array.new # These are the answers that the users picked up $intQuestionOnScreen = 0 # The current question on the screen $intTotalQuestions = 1 # Total number of questions # Opens up the quiz file and loads all the content into an array def openFile arrQuizContents = Array.new $filename = Tk::getOpenFile begin f = File.open($filename) or die "Unable to open file..." rescue # In case the users hits cancel or ESC, return to the menu return end arrQuizContents = [] f.each_line {|strLine| arrQuizContents.push strLine # Puts the file in one array } f.close ParseFile(arrQuizContents) $intQuestionOnScreen = 1 StartQuiz() end # Parses the contents array and gets question number lines, questions, possible answers and correct answers def ParseFile(arr) arr.each do |strLine| if strLine.match(/^[Qq][0-9]/) $arrQuestionLines.push strLine elsif strLine.match(/^[Aa]\./) $arrAnswerA.push strLine elsif strLine.match(/^[Bb]\./) $arrAnswerB.push strLine elsif strLine.match(/^[Cc]\./) $arrAnswerC.push strLine elsif strLine.match(/^[Dd]\./) $arrAnswerD.push strLine elsif strLine.match(/^Answer:/) $arrCorrectAnswers.push ((strLine.sub("Answer:", "")).gsub(" ","")).gsub(",","").gsub(/\n/,"") else $arrQuestions.push strLine end end $intTotalQuestions = $arrQuestionLines.length end # Initializes all the widgets and arranges them on the screen # Frame widgets start with frm # label widgets start with lbl # button widgets start with btn # grid_propagate(false) means that the frame is absolute. # The size (height, width) always stay the same def StartQuiz() frmQuestion = Tk::Tile::Frame.new($root) {width 760; height 175} frmQuestion.grid :column => 1, :row => 0, :columnspan => 4 frmQuestion.grid_propagate(false) frmAnswerA = Tk::Tile::Frame.new($root) {width 760; height 90} frmAnswerA.grid :column => 1, :row => 1, :columnspan => 4 frmAnswerA.grid_propagate(false) frmAnswerB = Tk::Tile::Frame.new($root) {width 760; height 90} frmAnswerB.grid :column => 1, :row => 2, :columnspan => 4 frmAnswerB.grid_propagate(false) frmAnswerC = Tk::Tile::Frame.new($root) {width 760; height 90} frmAnswerC.grid :column => 1, :row => 3, :columnspan => 4 frmAnswerC.grid_propagate(false) frmAnswerD = Tk::Tile::Frame.new($root) {width 760; height 90} frmAnswerD.grid :column => 1, :row => 4, :columnspan => 4 frmAnswerD.grid_propagate(false) $frmChoiceA = Tk::Tile::Frame.new($root) {width 40; height 90} $frmChoiceA.grid :column => 0, :row => 1 $frmChoiceA.grid_propagate(false) $frmChoiceB = Tk::Tile::Frame.new($root) {width 40; height 90} $frmChoiceB.grid :column => 0, :row => 2 $frmChoiceB.grid_propagate(false) $frmChoiceC = Tk::Tile::Frame.new($root) {width 40; height 90} $frmChoiceC.grid :column => 0, :row => 3 $frmChoiceC.grid_propagate(false) $frmChoiceD = Tk::Tile::Frame.new($root) {width 40; height 90} $frmChoiceD.grid :column => 0, :row => 4 $frmChoiceD.grid_propagate(false) frmNothing = Tk::Tile::Frame.new($root) {width 40; height 65} frmNothing.grid :column => 0, :row => 5, :rowspan => 2 frmNothing.grid_propagate(false) frmStats = Tk::Tile::Frame.new($root) {width 760; height 30} frmStats.grid :column => 1, :row => 6, :columnspan => 4 frmStats.grid_propagate(false) frmNumber = Tk::Tile::Frame.new($root) {width 40; height 175} frmNumber.grid :column => 0, :row => 0 frmNumber.grid_propagate(false) frmbtnAnswer = Tk::Tile::Frame.new($root) {width 190; height 35} frmbtnAnswer.grid :column => 1, :row => 5 frmbtnAnswer.grid_propagate(false) frmbtnPrev = Tk::Tile::Frame.new($root) {width 190; height 35} frmbtnPrev.grid :column => 2, :row => 5 frmbtnPrev.grid_propagate(false) frmbtnNext = Tk::Tile::Frame.new($root) {width 190; height 35} frmbtnNext.grid :column => 3, :row => 5 frmbtnNext.grid_propagate(false) frmbtnFinish = Tk::Tile::Frame.new($root) {width 190; height 35} frmbtnFinish.grid :column => 4, :row => 5 frmbtnFinish.grid_propagate(false) btnAnswer = Tk::Tile::Button.new(frmbtnAnswer) {text "Answer"; command {answerQuestion()}} $btnPrev = Tk::Tile::Button.new(frmbtnPrev) {text "< Prev"; command {prevQuestion()}} $btnPrev.state = 'disabled' $btnNext = Tk::Tile::Button.new(frmbtnNext) {text "Next >"; command {nextQuestion()}} btnFinish = Tk::Tile::Button.new(frmbtnFinish) {text "Finish"; command {finishQuestion()}} $lblQuestionNumber = Tk::Tile::Label.new(frmNumber) {text ""} $lblQuestionNumber['style'] = "QuestionNumber.TLabel" $lblStats = Tk::Tile::Label.new(frmStats) {text "Program by Kliment ANDREEV - 2015"} $lblStats['style'] = "Stats.TLabel" $lblStats.grid :column =>0 , :row => 0 $lblStats.text = $filename + " | Program by Kliment Andreev - 2015" $lblQuestion = Tk::Tile::Label.new(frmQuestion) {text "" ;wraplength 755 } $lblQuestion['style'] = "Question.TLabel" $lblAnswerA = Tk::Tile::Label.new(frmAnswerA) { text "" wraplength 755 }.grid( :column => 0, :row => 0, :sticky => 'w') $lblAnswerA['style'] = "Answer.TLabel" $lblAnswerB = Tk::Tile::Label.new(frmAnswerB) { text "" wraplength 755 }.grid( :column => 0, :row => 1, :sticky => 'w') $lblAnswerB['style'] = "Answer.TLabel" $lblAnswerC = Tk::Tile::Label.new(frmAnswerC) { text "" wraplength 755 }.grid( :column => 0, :row => 2, :sticky => 'w') $lblAnswerC['style'] = "Answer.TLabel" $lblAnswerD = Tk::Tile::Label.new(frmAnswerD) { text "" wraplength 755 }.grid( :column => 0, :row => 3, :sticky => 'w') $lblAnswerD['style'] = "Answer.TLabel" btnAnswer.grid :column => 0, :row => 0 $btnPrev.grid :column => 0, :row => 0 $btnNext.grid :column => 0, :row => 0 btnFinish.grid :column => 0, :row => 0 printQuestionOnScreen() printAnswersOnScreen() printChoicesOnScreen() end # Prints the question and the question number on the screen def printQuestionOnScreen() $lblQuestionNumber.grid :column => 0, :row => 0 $lblQuestionNumber['text'] = $intQuestionOnScreen.to_s + "." $lblQuestion.grid :column => 1, :row => 0 $lblQuestion['text'] = $arrQuestions[$intQuestionOnScreen - 1] end # Prints all four possible answers on the screen def printAnswersOnScreen() $lblAnswerA['text'] = $arrAnswerA[$intQuestionOnScreen - 1] $lblAnswerB['text'] = $arrAnswerB[$intQuestionOnScreen - 1] $lblAnswerC['text'] = $arrAnswerC[$intQuestionOnScreen - 1] $lblAnswerD['text'] = $arrAnswerD[$intQuestionOnScreen - 1] end # Destroys the choice widgets when the user chooses previous or next question def destroyChoicesOnScreen() if $strAnswer.length == 1 $radioA.destroy $radioB.destroy $radioC.destroy $radioD.destroy else $checkA.destroy $checkB.destroy $checkC.destroy $checkD.destroy end end # Prints the radio buttons or check buttons (choices) for the answers def printChoicesOnScreen() $strAnswer = ($arrCorrectAnswers[$intQuestionOnScreen - 1].strip) # Based on the number of correct answers, the program decides to use radio or check buttons if $strAnswer.length == 1 $radioChoice = TkVariable.new $radioA = Tk::Tile::RadioButton.new($frmChoiceA) {text ''; variable $radioChoice; value 'A'; command {yourAnswer}} $radioB = Tk::Tile::RadioButton.new($frmChoiceB) {text ''; variable $radioChoice; value 'B'; command {yourAnswer}} $radioC = Tk::Tile::RadioButton.new($frmChoiceC) {text ''; variable $radioChoice; value 'C'; command {yourAnswer}} $radioD = Tk::Tile::RadioButton.new($frmChoiceD) {text ''; variable $radioChoice; value 'D'; command {yourAnswer}} $radioA.grid :column => 0, :row => 0 $radioB.grid :column => 0, :row => 0 $radioC.grid :column => 0, :row => 0 $radioD.grid :column => 0, :row => 0 else $option_one = TkVariable.new( 0 ) $checkA = Tk::Tile::CheckButton.new($frmChoiceA) {text ""; variable $option_one; onvalue 'A'; command {yourAnswer}} $option_two = TkVariable.new( 0 ) $checkB = Tk::Tile::CheckButton.new($frmChoiceB) {text ""; variable $option_two; onvalue 'B'; command {yourAnswer}} $option_three = TkVariable.new( 0 ) $checkC = Tk::Tile::CheckButton.new($frmChoiceC) {text ""; variable $option_three; onvalue 'C'; command {yourAnswer}} $option_four = TkVariable.new( 0 ) $checkD = Tk::Tile::CheckButton.new($frmChoiceD) {text ""; variable $option_four; onvalue 'D'; command {yourAnswer}} $checkA.grid :column => 0, :row => 0 $checkB.grid :column => 0, :row => 0 $checkC.grid :column => 0, :row => 0 $checkD.grid :column => 0, :row => 0 end end # When user clicks the button for the next question def nextQuestion() return if $intQuestionOnScreen == 0 || $intQuestionOnScreen == $intTotalQuestions $intQuestionOnScreen += 1 if $intQuestionOnScreen == $intTotalQuestions $btnNext.state = 'disabled' end destroyChoicesOnScreen() resetAnswerLabelColors() printQuestionOnScreen() printAnswersOnScreen() printChoicesOnScreen() printUsersChoiceOnScreen() $btnPrev.state = 'enabled' end # Resets answers to the default look def resetAnswerLabelColors() $lblAnswerA['style'] = "Answer.TLabel" $lblAnswerB['style'] = "Answer.TLabel" $lblAnswerC['style'] = "Answer.TLabel" $lblAnswerD['style'] = "Answer.TLabel" end # When user clicks the button for the previous question def prevQuestion() return if $intQuestionOnScreen == 0 || $intQuestionOnScreen == 1 $btnNext.state = 'enabled' $intQuestionOnScreen -= 1 if $intQuestionOnScreen == 1 $btnPrev.state = 'disabled' end destroyChoicesOnScreen() resetAnswerLabelColors() printQuestionOnScreen() printAnswersOnScreen() printChoicesOnScreen() printUsersChoiceOnScreen() end # Prints whatever the user selected when the previous or next question is displayed def printUsersChoiceOnScreen() $strAnswer = ($arrCorrectAnswers[$intQuestionOnScreen - 1].strip) if $strAnswer.length == 1 case $arrUserAnswers[$intQuestionOnScreen - 1] when "A" $radioA.invoke() when "B" $radioB.invoke() when "C" $radioC.invoke() when "D" $radioD.invoke() end else case $arrUserAnswers[$intQuestionOnScreen - 1] when "A" checkInvoke(true, false, false, false) when "B" checkInvoke(false, true, false, false) when "C" checkInvoke(false, false, true, false) when "D" checkInvoke(false, false, false, true) when "AB" checkInvoke(true, true, false, false) when "AC" checkInvoke(true, false, true, false) when "AD" checkInvoke(true, false, false, true) when "BC" checkInvoke(false, true, true, false) when "BD" checkInvoke(false, true, false, true) when "CD" checkInvoke(false, false, true, true) when "ABC" checkInvoke(true, true, true, false) when "ABD" checkInvoke(true, true, false, true) when "ACD" checkInvoke(true, false, true, true) when "BCD" checkInvoke(false, true, true, true) when "ABCD" checkInvoke(true, true, true, true) end end end def selectAnswerA $checkA.invoke() end def selectAnswerB $checkB.invoke() end def selectAnswerC $checkC.invoke() end def selectAnswerD $checkD.invoke() end def checkInvoke(bA, bB, bC, bD) arr = [method(:selectAnswerA), method(:selectAnswerB), method(:selectAnswerC), method(:selectAnswerD)] arr[0].call if bA arr[1].call if bB arr[2].call if bC arr[3].call if bD end # Prints the correct answer on the screen when user clicks on the Answer button def answerQuestion() return if $intQuestionOnScreen == 0 case $strAnswer when "A" $lblAnswerA['style'] = "Correct.TLabel" when "B" $lblAnswerB['style'] = "Correct.TLabel" when "C" $lblAnswerC['style'] = "Correct.TLabel" when "D" $lblAnswerD['style'] = "Correct.TLabel" when "AB" $lblAnswerA['style'] = "Correct.TLabel" $lblAnswerB['style'] = "Correct.TLabel" when "AC" $lblAnswerA['style'] = "Correct.TLabel" $lblAnswerC['style'] = "Correct.TLabel" when "AD" $lblAnswerA['style'] = "Correct.TLabel" $lblAnswerD['style'] = "Correct.TLabel" when "BC" $lblAnswerB['style'] = "Correct.TLabel" $lblAnswerC['style'] = "Correct.TLabel" when "BD" $lblAnswerB['style'] = "Correct.TLabel" $lblAnswerD['style'] = "Correct.TLabel" when "CD" $lblAnswerC['style'] = "Correct.TLabel" $lblAnswerD['style'] = "Correct.TLabel" when "ABC" $lblAnswerA['style'] = "Correct.TLabel" $lblAnswerB['style'] = "Correct.TLabel" $lblAnswerC['style'] = "Correct.TLabel" when "ABD" $lblAnswerA['style'] = "Correct.TLabel" $lblAnswerB['style'] = "Correct.TLabel" $lblAnswerC['style'] = "Correct.TLabel" when "ACD" $lblAnswerA['style'] = "Correct.TLabel" $lblAnswerC['style'] = "Correct.TLabel" $lblAnswerD['style'] = "Correct.TLabel" when "BCD" $lblAnswerB['style'] = "Correct.TLabel" $lblAnswerC['style'] = "Correct.TLabel" $lblAnswerD['style'] = "Correct.TLabel" else $lblAnswerA['style'] ="Correct.TLabel" $lblAnswerB['style'] ="Correct.TLabel" $lblAnswerC['style'] ="Correct.TLabel" $lblAnswerD['style'] ="Correct.TLabel" end end # Finishes the quiz when the user clicks on the Finish button and displays the stats def finishQuestion() # Compares the arrays of correct answers and user's selected answers # and creates another array with correct (true) answers arrTrueAnswers = $arrUserAnswers.zip($arrCorrectAnswers).map { |x, y| x == y} x = arrTrueAnswers.count(true) msgBox = Tk.messageBox( 'type' => "ok", 'icon' => "info", 'title' => "GemQuiz - Stats", 'message' => "You have " + x.to_s + " correct answers out of " + $intTotalQuestions.to_s + ". That's " + ((x.to_f / $intTotalQuestions.to_f) * 100).round(2).to_s + "%.") end # Updates the array with the answer that the user selected def yourAnswer() if $strAnswer.length == 1 $arrUserAnswers[$intQuestionOnScreen - 1, 1] = $radioChoice else $arrUserAnswers[$intQuestionOnScreen - 1] = ($option_one.to_s + $option_two.to_s + $option_three.to_s + $option_four.to_s).gsub("0", "") end end # Exits the program if selected from the menu def exitProgram exit(0) end Tk.mainloop